blob: dac894035e30fd29c9efc5ccd22c0d004e413a71 [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 Ruelf24f09c2018-03-23 16:06:18 -04008__version__ = '0.10.3'
maruel@chromium.org0437a732013-08-27 16:05:52 +00009
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050010import collections
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -040011import datetime
maruel@chromium.org0437a732013-08-27 16:05:52 +000012import json
13import logging
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040014import optparse
maruel@chromium.org0437a732013-08-27 16:05:52 +000015import os
Tim 'mithro' 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
300
maruel77f720b2015-09-15 12:35:22 -0700301 STATES = (
302 'RUNNING', 'PENDING', 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED',
303 'COMPLETED')
304 STATES_RUNNING = ('RUNNING', 'PENDING')
305 STATES_NOT_RUNNING = (
306 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED', 'COMPLETED')
307 STATES_DONE = ('TIMED_OUT', 'COMPLETED')
308 STATES_ABANDONED = ('EXPIRED', 'BOT_DIED', 'CANCELED')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400309
310 _NAMES = {
311 RUNNING: 'Running',
312 PENDING: 'Pending',
313 EXPIRED: 'Expired',
314 TIMED_OUT: 'Execution timed out',
315 BOT_DIED: 'Bot died',
316 CANCELED: 'User canceled',
317 COMPLETED: 'Completed',
318 }
319
maruel77f720b2015-09-15 12:35:22 -0700320 _ENUMS = {
321 'RUNNING': RUNNING,
322 'PENDING': PENDING,
323 'EXPIRED': EXPIRED,
324 'TIMED_OUT': TIMED_OUT,
325 'BOT_DIED': BOT_DIED,
326 'CANCELED': CANCELED,
327 'COMPLETED': COMPLETED,
328 }
329
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400330 @classmethod
331 def to_string(cls, state):
332 """Returns a user-readable string representing a State."""
333 if state not in cls._NAMES:
334 raise ValueError('Invalid state %s' % state)
335 return cls._NAMES[state]
maruel@chromium.org0437a732013-08-27 16:05:52 +0000336
maruel77f720b2015-09-15 12:35:22 -0700337 @classmethod
338 def from_enum(cls, state):
339 """Returns int value based on the string."""
340 if state not in cls._ENUMS:
341 raise ValueError('Invalid state %s' % state)
342 return cls._ENUMS[state]
343
maruel@chromium.org0437a732013-08-27 16:05:52 +0000344
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700345class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700346 """Assembles task execution summary (for --task-summary-json output).
347
348 Optionally fetches task outputs from isolate server to local disk (used when
349 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700350
351 This object is shared among multiple threads running 'retrieve_results'
352 function, in particular they call 'process_shard_result' method in parallel.
353 """
354
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000355 def __init__(self, task_output_dir, task_output_stdout, shard_count):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700356 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
357
358 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700359 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700360 shard_count: expected number of task shards.
361 """
maruel12e30012015-10-09 11:55:35 -0700362 self.task_output_dir = (
363 unicode(os.path.abspath(task_output_dir))
364 if task_output_dir else task_output_dir)
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000365 self.task_output_stdout = task_output_stdout
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700366 self.shard_count = shard_count
367
368 self._lock = threading.Lock()
369 self._per_shard_results = {}
370 self._storage = None
371
nodire5028a92016-04-29 14:38:21 -0700372 if self.task_output_dir:
373 file_path.ensure_tree(self.task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700374
Vadim Shtayurab450c602014-05-12 19:23:25 -0700375 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700376 """Stores results of a single task shard, fetches output files if necessary.
377
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400378 Modifies |result| in place.
379
maruel77f720b2015-09-15 12:35:22 -0700380 shard_index is 0-based.
381
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700382 Called concurrently from multiple threads.
383 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700384 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700385 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700386 if shard_index < 0 or shard_index >= self.shard_count:
387 logging.warning(
388 'Shard index %d is outside of expected range: [0; %d]',
389 shard_index, self.shard_count - 1)
390 return
391
maruel77f720b2015-09-15 12:35:22 -0700392 if result.get('outputs_ref'):
393 ref = result['outputs_ref']
394 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
395 ref['isolatedserver'],
396 urllib.urlencode(
397 [('namespace', ref['namespace']), ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400398
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700399 # Store result dict of that shard, ignore results we've already seen.
400 with self._lock:
401 if shard_index in self._per_shard_results:
402 logging.warning('Ignoring duplicate shard index %d', shard_index)
403 return
404 self._per_shard_results[shard_index] = result
405
406 # Fetch output files if necessary.
maruel77f720b2015-09-15 12:35:22 -0700407 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400408 storage = self._get_storage(
maruel77f720b2015-09-15 12:35:22 -0700409 result['outputs_ref']['isolatedserver'],
410 result['outputs_ref']['namespace'])
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400411 if storage:
412 # Output files are supposed to be small and they are not reused across
413 # tasks. So use MemoryCache for them instead of on-disk cache. Make
414 # files writable, so that calling script can delete them.
415 isolateserver.fetch_isolated(
maruel77f720b2015-09-15 12:35:22 -0700416 result['outputs_ref']['isolated'],
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400417 storage,
418 isolateserver.MemoryCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -0700419 os.path.join(self.task_output_dir, str(shard_index)),
420 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700421
422 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700423 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700424 with self._lock:
425 # Write an array of shard results with None for missing shards.
426 summary = {
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700427 'shards': [
428 self._per_shard_results.get(i) for i in xrange(self.shard_count)
429 ],
430 }
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000431
432 # Don't store stdout in the summary if not requested too.
433 if "json" not in self.task_output_stdout:
434 for shard_json in summary['shards']:
435 if not shard_json:
436 continue
437 if "output" in shard_json:
438 del shard_json["output"]
439 if "outputs" in shard_json:
440 del shard_json["outputs"]
441
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700442 # Write summary.json to task_output_dir as well.
443 if self.task_output_dir:
444 tools.write_json(
maruel12e30012015-10-09 11:55:35 -0700445 os.path.join(self.task_output_dir, u'summary.json'),
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700446 summary,
447 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700448 if self._storage:
449 self._storage.close()
450 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700451 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700452
453 def _get_storage(self, isolate_server, namespace):
454 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700455 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700456 with self._lock:
457 if not self._storage:
458 self._storage = isolateserver.get_storage(isolate_server, namespace)
459 else:
460 # Shards must all use exact same isolate server and namespace.
461 if self._storage.location != isolate_server:
462 logging.error(
463 'Task shards are using multiple isolate servers: %s and %s',
464 self._storage.location, isolate_server)
465 return None
466 if self._storage.namespace != namespace:
467 logging.error(
468 'Task shards are using multiple namespaces: %s and %s',
469 self._storage.namespace, namespace)
470 return None
471 return self._storage
472
473
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500474def now():
475 """Exists so it can be mocked easily."""
476 return time.time()
477
478
maruel77f720b2015-09-15 12:35:22 -0700479def parse_time(value):
480 """Converts serialized time from the API to datetime.datetime."""
481 # When microseconds are 0, the '.123456' suffix is elided. This means the
482 # serialized format is not consistent, which confuses the hell out of python.
483 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
484 try:
485 return datetime.datetime.strptime(value, fmt)
486 except ValueError:
487 pass
488 raise ValueError('Failed to parse %s' % value)
489
490
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700491def retrieve_results(
maruel9531ce02016-04-13 06:11:23 -0700492 base_url, shard_index, task_id, timeout, should_stop, output_collector,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000493 include_perf, fetch_stdout):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400494 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700495
Vadim Shtayurab450c602014-05-12 19:23:25 -0700496 Returns:
497 <result dict> on success.
498 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700499 """
maruel71c61c82016-02-22 06:52:05 -0800500 assert timeout is None or isinstance(timeout, float), timeout
maruel380e3262016-08-31 16:10:06 -0700501 result_url = '%s/api/swarming/v1/task/%s/result' % (base_url, task_id)
maruel9531ce02016-04-13 06:11:23 -0700502 if include_perf:
503 result_url += '?include_performance_stats=true'
maruel380e3262016-08-31 16:10:06 -0700504 output_url = '%s/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700505 started = now()
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400506 deadline = started + timeout if timeout > 0 else None
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700507 attempt = 0
508
509 while not should_stop.is_set():
510 attempt += 1
511
512 # Waiting for too long -> give up.
513 current_time = now()
514 if deadline and current_time >= deadline:
515 logging.error('retrieve_results(%s) timed out on attempt %d',
516 base_url, attempt)
517 return None
518
519 # Do not spin too fast. Spin faster at the beginning though.
520 # Start with 1 sec delay and for each 30 sec of waiting add another second
521 # of delay, until hitting 15 sec ceiling.
522 if attempt > 1:
523 max_delay = min(15, 1 + (current_time - started) / 30.0)
524 delay = min(max_delay, deadline - current_time) if deadline else max_delay
525 if delay > 0:
526 logging.debug('Waiting %.1f sec before retrying', delay)
527 should_stop.wait(delay)
528 if should_stop.is_set():
529 return None
530
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400531 # Disable internal retries in net.url_read_json, since we are doing retries
532 # ourselves.
533 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700534 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
535 # request on GAE v2.
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400536 # Retry on 500s only if no timeout is specified.
537 result = net.url_read_json(result_url, retry_50x=bool(timeout == -1))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400538 if not result:
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400539 if timeout == -1:
540 return None
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400541 continue
maruel77f720b2015-09-15 12:35:22 -0700542
maruelbf53e042015-12-01 15:00:51 -0800543 if result.get('error'):
544 # An error occurred.
545 if result['error'].get('errors'):
546 for err in result['error']['errors']:
547 logging.warning(
548 'Error while reading task: %s; %s',
549 err.get('message'), err.get('debugInfo'))
550 elif result['error'].get('message'):
551 logging.warning(
552 'Error while reading task: %s', result['error']['message'])
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400553 if timeout == -1:
554 return result
maruelbf53e042015-12-01 15:00:51 -0800555 continue
556
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400557 # When timeout == -1, always return on first attempt. 500s are already
558 # retried in this case.
559 if result['state'] in State.STATES_NOT_RUNNING or timeout == -1:
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000560 if fetch_stdout:
561 out = net.url_read_json(output_url)
Vadim Shtayura6fd3c7b2017-11-03 15:32:51 -0700562 result['output'] = out.get('output', '') if out else ''
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700563 # Record the result, try to fetch attached output files (if any).
564 if output_collector:
565 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700566 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700567 if result.get('internal_failure'):
568 logging.error('Internal error!')
569 elif result['state'] == 'BOT_DIED':
570 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700571 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000572
573
maruel77f720b2015-09-15 12:35:22 -0700574def convert_to_old_format(result):
575 """Converts the task result data from Endpoints API format to old API format
576 for compatibility.
577
578 This goes into the file generated as --task-summary-json.
579 """
580 # Sets default.
581 result.setdefault('abandoned_ts', None)
582 result.setdefault('bot_id', None)
583 result.setdefault('bot_version', None)
584 result.setdefault('children_task_ids', [])
585 result.setdefault('completed_ts', None)
586 result.setdefault('cost_saved_usd', None)
587 result.setdefault('costs_usd', None)
588 result.setdefault('deduped_from', None)
589 result.setdefault('name', None)
590 result.setdefault('outputs_ref', None)
maruel77f720b2015-09-15 12:35:22 -0700591 result.setdefault('server_versions', None)
592 result.setdefault('started_ts', None)
593 result.setdefault('tags', None)
594 result.setdefault('user', None)
595
596 # Convertion back to old API.
597 duration = result.pop('duration', None)
598 result['durations'] = [duration] if duration else []
599 exit_code = result.pop('exit_code', None)
600 result['exit_codes'] = [int(exit_code)] if exit_code else []
601 result['id'] = result.pop('task_id')
602 result['isolated_out'] = result.get('outputs_ref', None)
603 output = result.pop('output', None)
604 result['outputs'] = [output] if output else []
maruel77f720b2015-09-15 12:35:22 -0700605 # server_version
606 # Endpoints result 'state' as string. For compatibility with old code, convert
607 # to int.
608 result['state'] = State.from_enum(result['state'])
maruel77f720b2015-09-15 12:35:22 -0700609 result['try_number'] = (
maruela4e8d752015-09-16 18:03:20 -0700610 int(result['try_number']) if result.get('try_number') else None)
maruel8786f2b2015-09-18 06:03:56 -0700611 if 'bot_dimensions' in result:
612 result['bot_dimensions'] = {
vadimsh72bf2532016-06-07 18:06:17 -0700613 i['key']: i.get('value', []) for i in result['bot_dimensions']
maruel8786f2b2015-09-18 06:03:56 -0700614 }
615 else:
616 result['bot_dimensions'] = None
maruel77f720b2015-09-15 12:35:22 -0700617
618
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700619def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400620 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000621 output_collector, include_perf, fetch_stdout):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500622 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000623
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700624 Duplicate shards are ignored. Shards are yielded in order of completion.
625 Timed out shards are NOT yielded at all. Caller can compare number of yielded
626 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000627
628 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500629 done. Since in general the number of task_keys is in the range <=10, it's not
maruel@chromium.org0437a732013-08-27 16:05:52 +0000630 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500631
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700632 output_collector is an optional instance of TaskOutputCollector that will be
633 used to fetch files produced by a task from isolate server to the local disk.
634
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500635 Yields:
636 (index, result). In particular, 'result' is defined as the
637 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000638 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000639 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400640 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700641 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700642 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700643
maruel@chromium.org0437a732013-08-27 16:05:52 +0000644 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
645 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700646 # Adds a task to the thread pool to call 'retrieve_results' and return
647 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400648 def enqueue_retrieve_results(shard_index, task_id):
Vadim Shtayurab450c602014-05-12 19:23:25 -0700649 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000650 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400651 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000652 task_id, timeout, should_stop, output_collector, include_perf,
653 fetch_stdout)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700654
655 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400656 for shard_index, task_id in enumerate(task_ids):
657 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700658
659 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400660 shards_remaining = range(len(task_ids))
661 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700662 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700663 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700664 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700665 shard_index, result = results_channel.pull(
666 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700667 except threading_utils.TaskChannel.Timeout:
668 if print_status_updates:
669 print(
670 'Waiting for results from the following shards: %s' %
671 ', '.join(map(str, shards_remaining)))
672 sys.stdout.flush()
673 continue
674 except Exception:
675 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700676
677 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700678 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000679 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500680 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000681 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700682
Vadim Shtayurab450c602014-05-12 19:23:25 -0700683 # Yield back results to the caller.
684 assert shard_index in shards_remaining
685 shards_remaining.remove(shard_index)
686 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700687
maruel@chromium.org0437a732013-08-27 16:05:52 +0000688 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700689 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000690 should_stop.set()
691
692
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000693def decorate_shard_output(swarming, shard_index, metadata, include_stdout):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000694 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700695 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400696 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700697 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
698 ).total_seconds()
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400699 elif (metadata.get('state') in ('BOT_DIED', 'CANCELED', 'EXPIRED') and
700 metadata.get('abandoned_ts')):
701 pending = '%.1fs' % (
702 parse_time(metadata['abandoned_ts']) -
703 parse_time(metadata['created_ts'])
704 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400705 else:
706 pending = 'N/A'
707
maruel77f720b2015-09-15 12:35:22 -0700708 if metadata.get('duration') is not None:
709 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400710 else:
711 duration = 'N/A'
712
maruel77f720b2015-09-15 12:35:22 -0700713 if metadata.get('exit_code') is not None:
714 # Integers are encoded as string to not loose precision.
715 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400716 else:
717 exit_code = 'N/A'
718
719 bot_id = metadata.get('bot_id') or 'N/A'
720
maruel77f720b2015-09-15 12:35:22 -0700721 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400722 tag_header = 'Shard %d %s' % (shard_index, url)
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000723 tag_footer1 = 'End of shard %d' % (shard_index)
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400724 if metadata.get('state') == 'CANCELED':
725 tag_footer2 = ' Pending: %s CANCELED' % pending
726 elif metadata.get('state') == 'EXPIRED':
727 tag_footer2 = ' Pending: %s EXPIRED (lack of capacity)' % pending
728 elif metadata.get('state') in ('BOT_DIED', 'TIMED_OUT'):
729 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s %s' % (
730 pending, duration, bot_id, exit_code, metadata['state'])
731 else:
732 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s' % (
733 pending, duration, bot_id, exit_code)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400734
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000735 tag_len = max(len(x) for x in [tag_header, tag_footer1, tag_footer2])
736 dash_pad = '+-%s-+' % ('-' * tag_len)
737 tag_header = '| %s |' % tag_header.ljust(tag_len)
738 tag_footer1 = '| %s |' % tag_footer1.ljust(tag_len)
739 tag_footer2 = '| %s |' % tag_footer2.ljust(tag_len)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400740
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000741 if include_stdout:
742 return '\n'.join([
743 dash_pad,
744 tag_header,
745 dash_pad,
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400746 (metadata.get('output') or '').rstrip(),
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000747 dash_pad,
748 tag_footer1,
749 tag_footer2,
750 dash_pad,
751 ])
752 else:
753 return '\n'.join([
754 dash_pad,
755 tag_header,
756 tag_footer2,
757 dash_pad,
758 ])
maruel@chromium.org0437a732013-08-27 16:05:52 +0000759
760
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700761def collect(
maruel0eb1d1b2015-10-02 14:48:21 -0700762 swarming, task_ids, timeout, decorate, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000763 task_summary_json, task_output_dir, task_output_stdout,
764 include_perf):
maruela5490782015-09-30 10:56:59 -0700765 """Retrieves results of a Swarming task.
766
767 Returns:
768 process exit code that should be returned to the user.
769 """
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700770 # Collect summary JSON and output files (if task_output_dir is not None).
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000771 output_collector = TaskOutputCollector(
772 task_output_dir, task_output_stdout, len(task_ids))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700773
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700774 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700775 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400776 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700777 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400778 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400779 swarming, task_ids, timeout, None, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000780 output_collector, include_perf,
781 (len(task_output_stdout) > 0),
782 ):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700783 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700784
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400785 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700786 shard_exit_code = metadata.get('exit_code')
787 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700788 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700789 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700790 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400791 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700792 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700793
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700794 if decorate:
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000795 s = decorate_shard_output(
796 swarming, index, metadata,
797 "console" in task_output_stdout).encode(
798 'utf-8', 'replace')
leileied181762016-10-13 14:24:59 -0700799 print(s)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400800 if len(seen_shards) < len(task_ids):
801 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700802 else:
maruel77f720b2015-09-15 12:35:22 -0700803 print('%s: %s %s' % (
804 metadata.get('bot_id', 'N/A'),
805 metadata['task_id'],
806 shard_exit_code))
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000807 if "console" in task_output_stdout and metadata['output']:
maruel77f720b2015-09-15 12:35:22 -0700808 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400809 if output:
810 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700811 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700812 summary = output_collector.finalize()
813 if task_summary_json:
maruel77f720b2015-09-15 12:35:22 -0700814 # TODO(maruel): Make this optional.
815 for i in summary['shards']:
816 if i:
817 convert_to_old_format(i)
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700818 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700819
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400820 if decorate and total_duration:
821 print('Total duration: %.1fs' % total_duration)
822
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400823 if len(seen_shards) != len(task_ids):
824 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700825 print >> sys.stderr, ('Results from some shards are missing: %s' %
826 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700827 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700828
maruela5490782015-09-30 10:56:59 -0700829 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000830
831
maruel77f720b2015-09-15 12:35:22 -0700832### API management.
833
834
835class APIError(Exception):
836 pass
837
838
839def endpoints_api_discovery_apis(host):
840 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
841 the APIs exposed by a host.
842
843 https://developers.google.com/discovery/v1/reference/apis/list
844 """
maruel380e3262016-08-31 16:10:06 -0700845 # Uses the real Cloud Endpoints. This needs to be fixed once the Cloud
846 # Endpoints version is turned down.
maruel77f720b2015-09-15 12:35:22 -0700847 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
848 if data is None:
849 raise APIError('Failed to discover APIs on %s' % host)
850 out = {}
851 for api in data['items']:
852 if api['id'] == 'discovery:v1':
853 continue
854 # URL is of the following form:
855 # url = host + (
856 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
857 api_data = net.url_read_json(api['discoveryRestUrl'])
858 if api_data is None:
859 raise APIError('Failed to discover %s on %s' % (api['id'], host))
860 out[api['id']] = api_data
861 return out
862
863
maruelaf6b06c2017-06-08 06:26:53 -0700864def get_yielder(base_url, limit):
865 """Returns the first query and a function that yields following items."""
866 CHUNK_SIZE = 250
867
868 url = base_url
869 if limit:
870 url += '%slimit=%d' % ('&' if '?' in url else '?', min(CHUNK_SIZE, limit))
871 data = net.url_read_json(url)
872 if data is None:
873 # TODO(maruel): Do basic diagnostic.
874 raise Failure('Failed to access %s' % url)
875 org_cursor = data.pop('cursor', None)
876 org_total = len(data.get('items') or [])
877 logging.info('get_yielder(%s) returning %d items', base_url, org_total)
878 if not org_cursor or not org_total:
879 # This is not an iterable resource.
880 return data, lambda: []
881
882 def yielder():
883 cursor = org_cursor
884 total = org_total
885 # Some items support cursors. Try to get automatically if cursors are needed
886 # by looking at the 'cursor' items.
887 while cursor and (not limit or total < limit):
888 merge_char = '&' if '?' in base_url else '?'
889 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(cursor))
890 if limit:
891 url += '&limit=%d' % min(CHUNK_SIZE, limit - total)
892 new = net.url_read_json(url)
893 if new is None:
894 raise Failure('Failed to access %s' % url)
895 cursor = new.get('cursor')
896 new_items = new.get('items')
897 nb_items = len(new_items or [])
898 total += nb_items
899 logging.info('get_yielder(%s) yielding %d items', base_url, nb_items)
900 yield new_items
901
902 return data, yielder
903
904
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500905### Commands.
906
907
908def abort_task(_swarming, _manifest):
909 """Given a task manifest that was triggered, aborts its execution."""
910 # TODO(vadimsh): No supported by the server yet.
911
912
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400913def add_filter_options(parser):
maruel681d6802017-01-17 16:56:03 -0800914 parser.filter_group = optparse.OptionGroup(parser, 'Bot selection')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500915 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500916 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500917 dest='dimensions', metavar='FOO bar',
918 help='dimension to filter on')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500919 parser.add_option_group(parser.filter_group)
920
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400921
maruelaf6b06c2017-06-08 06:26:53 -0700922def process_filter_options(parser, options):
923 for key, value in options.dimensions:
924 if ':' in key:
925 parser.error('--dimension key cannot contain ":"')
926 if key.strip() != key:
927 parser.error('--dimension key has whitespace')
928 if not key:
929 parser.error('--dimension key is empty')
930
931 if value.strip() != value:
932 parser.error('--dimension value has whitespace')
933 if not value:
934 parser.error('--dimension value is empty')
935 options.dimensions.sort()
936
937
Vadim Shtayurab450c602014-05-12 19:23:25 -0700938def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400939 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700940 parser.sharding_group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700941 '--shards', type='int', default=1, metavar='NUMBER',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700942 help='Number of shards to trigger and collect.')
943 parser.add_option_group(parser.sharding_group)
944
945
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400946def add_trigger_options(parser):
947 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500948 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400949 add_filter_options(parser)
950
maruel681d6802017-01-17 16:56:03 -0800951 group = optparse.OptionGroup(parser, 'Task properties')
952 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700953 '-s', '--isolated', metavar='HASH',
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500954 help='Hash of the .isolated to grab from the isolate server')
maruel681d6802017-01-17 16:56:03 -0800955 group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500956 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700957 help='Environment variables to set')
maruel681d6802017-01-17 16:56:03 -0800958 group.add_option(
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800959 '--env-prefix', default=[], action='append', nargs=2,
960 metavar='VAR local/path',
961 help='Prepend task-relative `local/path` to the task\'s VAR environment '
962 'variable using os-appropriate pathsep character. Can be specified '
963 'multiple times for the same VAR to add multiple paths.')
964 group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400965 '--idempotent', action='store_true', default=False,
966 help='When set, the server will actively try to find a previous task '
967 'with the same parameter and return this result instead if possible')
maruel681d6802017-01-17 16:56:03 -0800968 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700969 '--secret-bytes-path', metavar='FILE',
iannuccidc80dfb2016-10-28 12:50:20 -0700970 help='The optional path to a file containing the secret_bytes to use with'
971 'this task.')
maruel681d6802017-01-17 16:56:03 -0800972 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700973 '--hard-timeout', type='int', default=60*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400974 help='Seconds to allow the task to complete.')
maruel681d6802017-01-17 16:56:03 -0800975 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700976 '--io-timeout', type='int', default=20*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400977 help='Seconds to allow the task to be silent.')
maruel681d6802017-01-17 16:56:03 -0800978 group.add_option(
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500979 '--raw-cmd', action='store_true', default=False,
980 help='When set, the command after -- is used as-is without run_isolated. '
maruel0a25f6c2017-05-10 10:43:23 -0700981 'In this case, the .isolated file is expected to not have a command')
maruel681d6802017-01-17 16:56:03 -0800982 group.add_option(
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -0500983 '--relative-cwd',
984 help='Ignore the isolated \'relative_cwd\' and use this one instead; '
985 'requires --raw-cmd')
986 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700987 '--cipd-package', action='append', default=[], metavar='PKG',
988 help='CIPD packages to install on the Swarming bot. Uses the format: '
borenet02f772b2016-06-22 12:42:19 -0700989 'path:package_name:version')
maruel681d6802017-01-17 16:56:03 -0800990 group.add_option(
991 '--named-cache', action='append', nargs=2, default=[],
maruel5475ba62017-05-31 15:35:47 -0700992 metavar='NAME RELPATH',
maruel681d6802017-01-17 16:56:03 -0800993 help='"<name> <relpath>" items to keep a persistent bot managed cache')
994 group.add_option(
vadimsh93d167c2016-09-13 11:31:51 -0700995 '--service-account',
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700996 help='Email of a service account to run the task as, or literal "bot" '
997 'string to indicate that the task should use the same account the '
998 'bot itself is using to authenticate to Swarming. Don\'t use task '
999 'service accounts if not given (default).')
maruel681d6802017-01-17 16:56:03 -08001000 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001001 '-o', '--output', action='append', default=[], metavar='PATH',
1002 help='A list of files to return in addition to those written to '
1003 '${ISOLATED_OUTDIR}. An error will occur if a file specified by'
1004 'this option is also written directly to ${ISOLATED_OUTDIR}.')
maruel681d6802017-01-17 16:56:03 -08001005 parser.add_option_group(group)
1006
1007 group = optparse.OptionGroup(parser, 'Task request')
1008 group.add_option(
1009 '--priority', type='int', default=100,
1010 help='The lower value, the more important the task is')
1011 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001012 '-T', '--task-name', metavar='NAME',
maruel681d6802017-01-17 16:56:03 -08001013 help='Display name of the task. Defaults to '
1014 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
1015 'isolated file is provided, if a hash is provided, it defaults to '
1016 '<user>/<dimensions>/<isolated hash>/<timestamp>')
1017 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001018 '--tags', action='append', default=[], metavar='FOO:BAR',
maruel681d6802017-01-17 16:56:03 -08001019 help='Tags to assign to the task.')
1020 group.add_option(
1021 '--user', default='',
1022 help='User associated with the task. Defaults to authenticated user on '
1023 'the server.')
1024 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001025 '--expiration', type='int', default=6*60*60, metavar='SECS',
maruel681d6802017-01-17 16:56:03 -08001026 help='Seconds to allow the task to be pending for a bot to run before '
1027 'this task request expires.')
1028 group.add_option(
1029 '--deadline', type='int', dest='expiration',
1030 help=optparse.SUPPRESS_HELP)
1031 parser.add_option_group(group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001032
1033
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001034def process_trigger_options(parser, options, args):
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001035 """Processes trigger options and does preparatory steps."""
maruelaf6b06c2017-06-08 06:26:53 -07001036 process_filter_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001037 options.env = dict(options.env)
maruel0a25f6c2017-05-10 10:43:23 -07001038 if args and args[0] == '--':
1039 args = args[1:]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001040
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001041 if not options.dimensions:
1042 parser.error('Please at least specify one --dimension')
maruel0a25f6c2017-05-10 10:43:23 -07001043 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
1044 parser.error('--tags must be in the format key:value')
1045 if options.raw_cmd and not args:
1046 parser.error(
1047 'Arguments with --raw-cmd should be passed after -- as command '
1048 'delimiter.')
1049 if options.isolate_server and not options.namespace:
1050 parser.error(
1051 '--namespace must be a valid value when --isolate-server is used')
1052 if not options.isolated and not options.raw_cmd:
1053 parser.error('Specify at least one of --raw-cmd or --isolated or both')
1054
1055 # Isolated
1056 # --isolated is required only if --raw-cmd wasn't provided.
1057 # TODO(maruel): --isolate-server may be optional as Swarming may have its own
1058 # preferred server.
1059 isolateserver.process_isolate_server_options(
1060 parser, options, False, not options.raw_cmd)
1061 inputs_ref = None
1062 if options.isolate_server:
1063 inputs_ref = FilesRef(
1064 isolated=options.isolated,
1065 isolatedserver=options.isolate_server,
1066 namespace=options.namespace)
1067
1068 # Command
1069 command = None
1070 extra_args = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001071 if options.raw_cmd:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001072 command = args
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001073 if options.relative_cwd:
1074 a = os.path.normpath(os.path.abspath(options.relative_cwd))
1075 if not a.startswith(os.getcwd()):
1076 parser.error(
1077 '--relative-cwd must not try to escape the working directory')
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001078 else:
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001079 if options.relative_cwd:
1080 parser.error('--relative-cwd requires --raw-cmd')
maruel0a25f6c2017-05-10 10:43:23 -07001081 extra_args = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001082
maruel0a25f6c2017-05-10 10:43:23 -07001083 # CIPD
borenet02f772b2016-06-22 12:42:19 -07001084 cipd_packages = []
1085 for p in options.cipd_package:
1086 split = p.split(':', 2)
1087 if len(split) != 3:
1088 parser.error('CIPD packages must take the form: path:package:version')
1089 cipd_packages.append(CipdPackage(
1090 package_name=split[1],
1091 path=split[0],
1092 version=split[2]))
1093 cipd_input = None
1094 if cipd_packages:
1095 cipd_input = CipdInput(
1096 client_package=None,
1097 packages=cipd_packages,
1098 server=None)
1099
maruel0a25f6c2017-05-10 10:43:23 -07001100 # Secrets
iannuccidc80dfb2016-10-28 12:50:20 -07001101 secret_bytes = None
1102 if options.secret_bytes_path:
1103 with open(options.secret_bytes_path, 'r') as f:
1104 secret_bytes = f.read().encode('base64')
1105
maruel0a25f6c2017-05-10 10:43:23 -07001106 # Named caches
maruel681d6802017-01-17 16:56:03 -08001107 caches = [
1108 {u'name': unicode(i[0]), u'path': unicode(i[1])}
1109 for i in options.named_cache
1110 ]
maruel0a25f6c2017-05-10 10:43:23 -07001111
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001112 env_prefixes = {}
1113 for k, v in options.env_prefix:
1114 env_prefixes.setdefault(k, []).append(v)
1115
maruel77f720b2015-09-15 12:35:22 -07001116 properties = TaskProperties(
maruel681d6802017-01-17 16:56:03 -08001117 caches=caches,
borenet02f772b2016-06-22 12:42:19 -07001118 cipd_input=cipd_input,
maruel0a25f6c2017-05-10 10:43:23 -07001119 command=command,
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001120 relative_cwd=options.relative_cwd,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001121 dimensions=options.dimensions,
1122 env=options.env,
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001123 env_prefixes=[StringListPair(k, v) for k, v in env_prefixes.iteritems()],
maruel77f720b2015-09-15 12:35:22 -07001124 execution_timeout_secs=options.hard_timeout,
maruel0a25f6c2017-05-10 10:43:23 -07001125 extra_args=extra_args,
maruel77f720b2015-09-15 12:35:22 -07001126 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001127 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -07001128 inputs_ref=inputs_ref,
aludwincc5524e2016-10-28 10:25:24 -07001129 io_timeout_secs=options.io_timeout,
iannuccidc80dfb2016-10-28 12:50:20 -07001130 outputs=options.output,
1131 secret_bytes=secret_bytes)
vadimsh93d167c2016-09-13 11:31:51 -07001132
maruel77f720b2015-09-15 12:35:22 -07001133 return NewTaskRequest(
1134 expiration_secs=options.expiration,
maruel0a25f6c2017-05-10 10:43:23 -07001135 name=default_task_name(options),
maruel77f720b2015-09-15 12:35:22 -07001136 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001137 priority=options.priority,
maruel77f720b2015-09-15 12:35:22 -07001138 properties=properties,
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001139 service_account=options.service_account,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001140 tags=options.tags,
maruel77f720b2015-09-15 12:35:22 -07001141 user=options.user)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001142
1143
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001144class TaskOutputStdoutOption(optparse.Option):
1145 """Where to output the each task's console output (stderr/stdout).
1146
1147 The output will be;
1148 none - not be downloaded.
1149 json - stored in summary.json file *only*.
1150 console - shown on stdout *only*.
1151 all - stored in summary.json and shown on stdout.
1152 """
1153
1154 choices = ['all', 'json', 'console', 'none']
1155
1156 def __init__(self, *args, **kw):
1157 optparse.Option.__init__(
1158 self,
1159 *args,
1160 choices=self.choices,
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001161 default=['console', 'json'],
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001162 help=re.sub('\s\s*', ' ', self.__doc__),
1163 **kw)
1164
1165 def convert_value(self, opt, value):
1166 if value not in self.choices:
1167 raise optparse.OptionValueError("%s must be one of %s not %r" % (
1168 self.get_opt_string(), self.choices, value))
1169 stdout_to = []
1170 if value == 'all':
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001171 stdout_to = ['console', 'json']
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001172 elif value != 'none':
1173 stdout_to = [value]
1174 return stdout_to
1175
1176
maruel@chromium.org0437a732013-08-27 16:05:52 +00001177def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001178 parser.server_group.add_option(
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001179 '-t', '--timeout', type='float', default=0,
1180 help='Timeout to wait for result, set to -1 for no timeout and get '
1181 'current state; defaults to waiting until the task completes')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001182 parser.group_logging.add_option(
1183 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001184 parser.group_logging.add_option(
1185 '--print-status-updates', action='store_true',
1186 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001187 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001188 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001189 '--task-summary-json',
1190 metavar='FILE',
1191 help='Dump a summary of task results to this file as json. It contains '
1192 'only shards statuses as know to server directly. Any output files '
1193 'emitted by the task can be collected by using --task-output-dir')
1194 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001195 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001196 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001197 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001198 'directory contains per-shard directory with output files produced '
1199 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001200 parser.task_output_group.add_option(TaskOutputStdoutOption(
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001201 '--task-output-stdout'))
maruel9531ce02016-04-13 06:11:23 -07001202 parser.task_output_group.add_option(
1203 '--perf', action='store_true', default=False,
1204 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001205 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001206
1207
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001208def process_collect_options(parser, options):
1209 # Only negative -1 is allowed, disallow other negative values.
1210 if options.timeout != -1 and options.timeout < 0:
1211 parser.error('Invalid --timeout value')
1212
1213
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001214@subcommand.usage('bots...')
1215def CMDbot_delete(parser, args):
1216 """Forcibly deletes bots from the Swarming server."""
1217 parser.add_option(
1218 '-f', '--force', action='store_true',
1219 help='Do not prompt for confirmation')
1220 options, args = parser.parse_args(args)
1221 if not args:
maruelfd0a90c2016-06-10 11:51:10 -07001222 parser.error('Please specify bots to delete')
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001223
1224 bots = sorted(args)
1225 if not options.force:
1226 print('Delete the following bots?')
1227 for bot in bots:
1228 print(' %s' % bot)
1229 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1230 print('Goodbye.')
1231 return 1
1232
1233 result = 0
1234 for bot in bots:
maruel380e3262016-08-31 16:10:06 -07001235 url = '%s/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
vadimshe4c0e242015-09-30 11:53:54 -07001236 if net.url_read_json(url, data={}, method='POST') is None:
1237 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001238 result = 1
1239 return result
1240
1241
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001242def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001243 """Returns information about the bots connected to the Swarming server."""
1244 add_filter_options(parser)
1245 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001246 '--dead-only', action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001247 help='Filter out bots alive, useful to reap them and reimage broken bots')
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001248 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001249 '-k', '--keep-dead', action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001250 help='Keep both dead and alive bots')
1251 parser.filter_group.add_option(
1252 '--busy', action='store_true', help='Keep only busy bots')
1253 parser.filter_group.add_option(
1254 '--idle', action='store_true', help='Keep only idle bots')
1255 parser.filter_group.add_option(
1256 '--mp', action='store_true',
1257 help='Keep only Machine Provider managed bots')
1258 parser.filter_group.add_option(
1259 '--non-mp', action='store_true',
1260 help='Keep only non Machine Provider managed bots')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001261 parser.filter_group.add_option(
1262 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001263 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001264 options, args = parser.parse_args(args)
maruelaf6b06c2017-06-08 06:26:53 -07001265 process_filter_options(parser, options)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001266
1267 if options.keep_dead and options.dead_only:
maruelaf6b06c2017-06-08 06:26:53 -07001268 parser.error('Use only one of --keep-dead or --dead-only')
1269 if options.busy and options.idle:
1270 parser.error('Use only one of --busy or --idle')
1271 if options.mp and options.non_mp:
1272 parser.error('Use only one of --mp or --non-mp')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001273
maruelaf6b06c2017-06-08 06:26:53 -07001274 url = options.swarming + '/api/swarming/v1/bots/list?'
1275 values = []
1276 if options.dead_only:
1277 values.append(('is_dead', 'TRUE'))
1278 elif options.keep_dead:
1279 values.append(('is_dead', 'NONE'))
1280 else:
1281 values.append(('is_dead', 'FALSE'))
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001282
maruelaf6b06c2017-06-08 06:26:53 -07001283 if options.busy:
1284 values.append(('is_busy', 'TRUE'))
1285 elif options.idle:
1286 values.append(('is_busy', 'FALSE'))
1287 else:
1288 values.append(('is_busy', 'NONE'))
1289
1290 if options.mp:
1291 values.append(('is_mp', 'TRUE'))
1292 elif options.non_mp:
1293 values.append(('is_mp', 'FALSE'))
1294 else:
1295 values.append(('is_mp', 'NONE'))
1296
1297 for key, value in options.dimensions:
1298 values.append(('dimensions', '%s:%s' % (key, value)))
1299 url += urllib.urlencode(values)
1300 try:
1301 data, yielder = get_yielder(url, 0)
1302 bots = data.get('items') or []
1303 for items in yielder():
1304 if items:
1305 bots.extend(items)
1306 except Failure as e:
1307 sys.stderr.write('\n%s\n' % e)
1308 return 1
maruel77f720b2015-09-15 12:35:22 -07001309 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
maruelaf6b06c2017-06-08 06:26:53 -07001310 print bot['bot_id']
1311 if not options.bare:
1312 dimensions = {i['key']: i.get('value') for i in bot.get('dimensions', {})}
1313 print ' %s' % json.dumps(dimensions, sort_keys=True)
1314 if bot.get('task_id'):
1315 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001316 return 0
1317
1318
maruelfd0a90c2016-06-10 11:51:10 -07001319@subcommand.usage('task_id')
1320def CMDcancel(parser, args):
1321 """Cancels a task."""
1322 options, args = parser.parse_args(args)
1323 if not args:
1324 parser.error('Please specify the task to cancel')
1325 for task_id in args:
maruel380e3262016-08-31 16:10:06 -07001326 url = '%s/api/swarming/v1/task/%s/cancel' % (options.swarming, task_id)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001327 resp = net.url_read_json(url, data={}, method='POST')
1328 if resp is None:
maruelfd0a90c2016-06-10 11:51:10 -07001329 print('Deleting %s failed. Probably already gone' % task_id)
1330 return 1
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001331 logging.info('%s', resp)
maruelfd0a90c2016-06-10 11:51:10 -07001332 return 0
1333
1334
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001335@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001336def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001337 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001338
1339 The result can be in multiple part if the execution was sharded. It can
1340 potentially have retries.
1341 """
1342 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001343 parser.add_option(
1344 '-j', '--json',
1345 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001346 options, args = parser.parse_args(args)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001347 process_collect_options(parser, options)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001348 if not args and not options.json:
1349 parser.error('Must specify at least one task id or --json.')
1350 if args and options.json:
1351 parser.error('Only use one of task id or --json.')
1352
1353 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001354 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001355 try:
maruel1ceb3872015-10-14 06:10:44 -07001356 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001357 data = json.load(f)
1358 except (IOError, ValueError):
1359 parser.error('Failed to open %s' % options.json)
1360 try:
1361 tasks = sorted(
1362 data['tasks'].itervalues(), key=lambda x: x['shard_index'])
1363 args = [t['task_id'] for t in tasks]
1364 except (KeyError, TypeError):
1365 parser.error('Failed to process %s' % options.json)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001366 if not options.timeout:
maruel71c61c82016-02-22 06:52:05 -08001367 options.timeout = (
1368 data['request']['properties']['execution_timeout_secs'] +
1369 data['request']['expiration_secs'] + 10.)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001370 else:
1371 valid = frozenset('0123456789abcdef')
1372 if any(not valid.issuperset(task_id) for task_id in args):
1373 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001374
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001375 try:
1376 return collect(
1377 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001378 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001379 options.timeout,
1380 options.decorate,
1381 options.print_status_updates,
1382 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001383 options.task_output_dir,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001384 options.task_output_stdout,
maruel9531ce02016-04-13 06:11:23 -07001385 options.perf)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001386 except Failure:
1387 on_error.report(None)
1388 return 1
1389
1390
maruel77f720b2015-09-15 12:35:22 -07001391@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001392def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001393 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1394 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001395
1396 Examples:
maruelaf6b06c2017-06-08 06:26:53 -07001397 Raw task request and results:
1398 swarming.py query -S server-url.com task/123456/request
1399 swarming.py query -S server-url.com task/123456/result
1400
maruel77f720b2015-09-15 12:35:22 -07001401 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001402 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001403
maruelaf6b06c2017-06-08 06:26:53 -07001404 Listing last 10 tasks on a specific bot named 'bot1':
1405 swarming.py query -S server-url.com --limit 10 bot/bot1/tasks
maruel84e77aa2015-10-21 06:37:24 -07001406
maruelaf6b06c2017-06-08 06:26:53 -07001407 Listing last 10 tasks with tags os:Ubuntu-14.04 and pool:Chrome. Note that
maruel84e77aa2015-10-21 06:37:24 -07001408 quoting is important!:
1409 swarming.py query -S server-url.com --limit 10 \\
maruelaf6b06c2017-06-08 06:26:53 -07001410 'tasks/list?tags=os:Ubuntu-14.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001411 """
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001412 parser.add_option(
1413 '-L', '--limit', type='int', default=200,
1414 help='Limit to enforce on limitless items (like number of tasks); '
1415 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001416 parser.add_option(
1417 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001418 parser.add_option(
1419 '--progress', action='store_true',
1420 help='Prints a dot at each request to show progress')
1421 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001422 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001423 parser.error(
1424 'Must specify only method name and optionally query args properly '
1425 'escaped.')
maruel380e3262016-08-31 16:10:06 -07001426 base_url = options.swarming + '/api/swarming/v1/' + args[0]
maruelaf6b06c2017-06-08 06:26:53 -07001427 try:
1428 data, yielder = get_yielder(base_url, options.limit)
1429 for items in yielder():
1430 if items:
1431 data['items'].extend(items)
maruel77f720b2015-09-15 12:35:22 -07001432 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001433 sys.stderr.write('.')
1434 sys.stderr.flush()
1435 except Failure as e:
1436 sys.stderr.write('\n%s\n' % e)
1437 return 1
maruel77f720b2015-09-15 12:35:22 -07001438 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001439 sys.stderr.write('\n')
1440 sys.stderr.flush()
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001441 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001442 options.json = unicode(os.path.abspath(options.json))
1443 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001444 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001445 try:
maruel77f720b2015-09-15 12:35:22 -07001446 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001447 sys.stdout.write('\n')
1448 except IOError:
1449 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001450 return 0
1451
1452
maruel77f720b2015-09-15 12:35:22 -07001453def CMDquery_list(parser, args):
1454 """Returns list of all the Swarming APIs that can be used with command
1455 'query'.
1456 """
1457 parser.add_option(
1458 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1459 options, args = parser.parse_args(args)
1460 if args:
1461 parser.error('No argument allowed.')
1462
1463 try:
1464 apis = endpoints_api_discovery_apis(options.swarming)
1465 except APIError as e:
1466 parser.error(str(e))
1467 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001468 options.json = unicode(os.path.abspath(options.json))
1469 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001470 json.dump(apis, f)
1471 else:
1472 help_url = (
1473 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1474 options.swarming)
maruel11e31af2017-02-15 07:30:50 -08001475 for i, (api_id, api) in enumerate(sorted(apis.iteritems())):
1476 if i:
1477 print('')
maruel77f720b2015-09-15 12:35:22 -07001478 print api_id
maruel11e31af2017-02-15 07:30:50 -08001479 print ' ' + api['description'].strip()
1480 if 'resources' in api:
1481 # Old.
1482 for j, (resource_name, resource) in enumerate(
1483 sorted(api['resources'].iteritems())):
1484 if j:
1485 print('')
1486 for method_name, method in sorted(resource['methods'].iteritems()):
1487 # Only list the GET ones.
1488 if method['httpMethod'] != 'GET':
1489 continue
1490 print '- %s.%s: %s' % (
1491 resource_name, method_name, method['path'])
1492 print('\n'.join(
1493 ' ' + l for l in textwrap.wrap(method['description'], 78)))
1494 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1495 else:
1496 # New.
1497 for method_name, method in sorted(api['methods'].iteritems()):
maruel77f720b2015-09-15 12:35:22 -07001498 # Only list the GET ones.
1499 if method['httpMethod'] != 'GET':
1500 continue
maruel11e31af2017-02-15 07:30:50 -08001501 print '- %s: %s' % (method['id'], method['path'])
1502 print('\n'.join(
1503 ' ' + l for l in textwrap.wrap(method['description'], 78)))
maruel77f720b2015-09-15 12:35:22 -07001504 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1505 return 0
1506
1507
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001508@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001509def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001510 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001511
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001512 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001513 """
1514 add_trigger_options(parser)
1515 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001516 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001517 options, args = parser.parse_args(args)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001518 process_collect_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001519 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001520 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001521 tasks = trigger_task_shards(
1522 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001523 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001524 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001525 'Failed to trigger %s(%s): %s' %
maruel0a25f6c2017-05-10 10:43:23 -07001526 (task_request.name, args[0], e.args[0]))
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001527 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001528 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001529 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001530 return 1
maruel0a25f6c2017-05-10 10:43:23 -07001531 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001532 task_ids = [
1533 t['task_id']
1534 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1535 ]
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001536 if not options.timeout:
maruel71c61c82016-02-22 06:52:05 -08001537 options.timeout = (
1538 task_request.properties.execution_timeout_secs +
1539 task_request.expiration_secs + 10.)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001540 try:
1541 return collect(
1542 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001543 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001544 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001545 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001546 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001547 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001548 options.task_output_dir,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001549 options.task_output_stdout,
maruel9531ce02016-04-13 06:11:23 -07001550 options.perf)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001551 except Failure:
1552 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001553 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001554
1555
maruel18122c62015-10-23 06:31:23 -07001556@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001557def CMDreproduce(parser, args):
1558 """Runs a task locally that was triggered on the server.
1559
1560 This running locally the same commands that have been run on the bot. The data
1561 downloaded will be in a subdirectory named 'work' of the current working
1562 directory.
maruel18122c62015-10-23 06:31:23 -07001563
1564 You can pass further additional arguments to the target command by passing
1565 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001566 """
maruelc070e672016-02-22 17:32:57 -08001567 parser.add_option(
maruel7f63a272016-07-12 12:40:36 -07001568 '--output-dir', metavar='DIR', default='out',
maruelc070e672016-02-22 17:32:57 -08001569 help='Directory that will have results stored into')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001570 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001571 extra_args = []
1572 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001573 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001574 if len(args) > 1:
1575 if args[1] == '--':
1576 if len(args) > 2:
1577 extra_args = args[2:]
1578 else:
1579 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001580
maruel380e3262016-08-31 16:10:06 -07001581 url = options.swarming + '/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001582 request = net.url_read_json(url)
1583 if not request:
1584 print >> sys.stderr, 'Failed to retrieve request data for the task'
1585 return 1
1586
maruel12e30012015-10-09 11:55:35 -07001587 workdir = unicode(os.path.abspath('work'))
maruele7cd38e2016-03-01 19:12:48 -08001588 if fs.isdir(workdir):
1589 parser.error('Please delete the directory \'work\' first')
1590 fs.mkdir(workdir)
iannucci31ab9192017-05-02 19:11:56 -07001591 cachedir = unicode(os.path.abspath('cipd_cache'))
1592 if not fs.exists(cachedir):
1593 fs.mkdir(cachedir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001594
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001595 properties = request['properties']
iannucci31ab9192017-05-02 19:11:56 -07001596 env = os.environ.copy()
1597 env['SWARMING_BOT_ID'] = 'reproduce'
1598 env['SWARMING_TASK_ID'] = 'reproduce'
maruel29ab2fd2015-10-16 11:44:01 -07001599 if properties.get('env'):
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001600 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001601 for i in properties['env']:
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001602 key = i['key']
maruelb76604c2015-11-11 11:53:44 -08001603 if not i['value']:
1604 env.pop(key, None)
1605 else:
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001606 env[key] = i['value']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001607
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001608 if properties.get('env_prefixes'):
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001609 env_prefixes = properties['env_prefixes']
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001610 logging.info('env_prefixes: %r', env_prefixes)
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001611 for i in env_prefixes:
1612 key = i['key']
1613 paths = [os.path.normpath(os.path.join(workdir, p)) for p in i['value']]
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001614 cur = env.get(key)
1615 if cur:
1616 paths.append(cur)
1617 env[key] = os.path.pathsep.join(paths)
1618
iannucci31ab9192017-05-02 19:11:56 -07001619 command = []
nodir152cba62016-05-12 16:08:56 -07001620 if (properties.get('inputs_ref') or {}).get('isolated'):
maruel29ab2fd2015-10-16 11:44:01 -07001621 # Create the tree.
1622 with isolateserver.get_storage(
1623 properties['inputs_ref']['isolatedserver'],
1624 properties['inputs_ref']['namespace']) as storage:
1625 bundle = isolateserver.fetch_isolated(
1626 properties['inputs_ref']['isolated'],
1627 storage,
1628 isolateserver.MemoryCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -07001629 workdir,
1630 False)
maruel29ab2fd2015-10-16 11:44:01 -07001631 command = bundle.command
1632 if bundle.relative_cwd:
1633 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001634 command.extend(properties.get('extra_args') or [])
iannucci31ab9192017-05-02 19:11:56 -07001635
1636 if properties.get('command'):
1637 command.extend(properties['command'])
1638
1639 # https://github.com/luci/luci-py/blob/master/appengine/swarming/doc/Magic-Values.md
Robert Iannucci24ae76a2018-02-26 12:51:18 -08001640 command = tools.fix_python_cmd(command, env)
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001641 if not options.output_dir:
1642 new_command = run_isolated.process_command(command, 'invalid', None)
1643 if new_command != command:
Marc-Antoine Ruel29ba75c2018-01-10 15:04:14 -05001644 parser.error('The task has outputs, you must use --output-dir')
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001645 else:
1646 # Make the path absolute, as the process will run from a subdirectory.
1647 options.output_dir = os.path.abspath(options.output_dir)
1648 new_command = run_isolated.process_command(
1649 command, options.output_dir, None)
Marc-Antoine Ruel29ba75c2018-01-10 15:04:14 -05001650 if not os.path.isdir(options.output_dir):
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001651 os.makedirs(options.output_dir)
iannucci31ab9192017-05-02 19:11:56 -07001652 command = new_command
1653 file_path.ensure_command_has_abs_path(command, workdir)
1654
1655 if properties.get('cipd_input'):
1656 ci = properties['cipd_input']
1657 cp = ci['client_package']
1658 client_manager = cipd.get_client(
1659 ci['server'], cp['package_name'], cp['version'], cachedir)
1660
1661 with client_manager as client:
1662 by_path = collections.defaultdict(list)
1663 for pkg in ci['packages']:
1664 path = pkg['path']
1665 # cipd deals with 'root' as ''
1666 if path == '.':
1667 path = ''
1668 by_path[path].append((pkg['package_name'], pkg['version']))
1669 client.ensure(workdir, by_path, cache_dir=cachedir)
1670
maruel77f720b2015-09-15 12:35:22 -07001671 try:
Marc-Antoine Ruel95c21872018-01-10 14:24:28 -05001672 return subprocess42.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001673 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001674 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001675 print >> sys.stderr, str(e)
1676 return 1
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001677
1678
maruel0eb1d1b2015-10-02 14:48:21 -07001679@subcommand.usage('bot_id')
1680def CMDterminate(parser, args):
1681 """Tells a bot to gracefully shut itself down as soon as it can.
1682
1683 This is done by completing whatever current task there is then exiting the bot
1684 process.
1685 """
1686 parser.add_option(
1687 '--wait', action='store_true', help='Wait for the bot to terminate')
1688 options, args = parser.parse_args(args)
1689 if len(args) != 1:
1690 parser.error('Please provide the bot id')
maruel380e3262016-08-31 16:10:06 -07001691 url = options.swarming + '/api/swarming/v1/bot/%s/terminate' % args[0]
maruel0eb1d1b2015-10-02 14:48:21 -07001692 request = net.url_read_json(url, data={})
1693 if not request:
1694 print >> sys.stderr, 'Failed to ask for termination'
1695 return 1
1696 if options.wait:
1697 return collect(
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001698 options.swarming,
1699 [request['task_id']],
1700 0.,
1701 False,
1702 False,
1703 None,
1704 None,
1705 [],
maruel9531ce02016-04-13 06:11:23 -07001706 False)
maruelbfc5f872017-06-10 16:43:17 -07001707 else:
1708 print request['task_id']
maruel0eb1d1b2015-10-02 14:48:21 -07001709 return 0
1710
1711
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001712@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001713def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001714 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001715
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001716 Passes all extra arguments provided after '--' as additional command line
1717 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001718 """
1719 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001720 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001721 parser.add_option(
1722 '--dump-json',
1723 metavar='FILE',
1724 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001725 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001726 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001727 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001728 tasks = trigger_task_shards(
1729 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001730 if tasks:
maruel0a25f6c2017-05-10 10:43:23 -07001731 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001732 tasks_sorted = sorted(
1733 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001734 if options.dump_json:
1735 data = {
maruel0a25f6c2017-05-10 10:43:23 -07001736 'base_task_name': task_request.name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001737 'tasks': tasks,
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001738 'request': task_request_to_raw_request(task_request),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001739 }
maruel46b015f2015-10-13 18:40:35 -07001740 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001741 print('To collect results, use:')
1742 print(' swarming.py collect -S %s --json %s' %
1743 (options.swarming, options.dump_json))
1744 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001745 print('To collect results, use:')
1746 print(' swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001747 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1748 print('Or visit:')
1749 for t in tasks_sorted:
1750 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001751 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001752 except Failure:
1753 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001754 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001755
1756
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001757class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001758 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001759 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001760 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001761 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001762 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001763 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001764 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001765 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001766 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001767 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001768
1769 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001770 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001771 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001772 auth.process_auth_options(self, options)
1773 user = self._process_swarming(options)
1774 if hasattr(options, 'user') and not options.user:
1775 options.user = user
1776 return options, args
1777
1778 def _process_swarming(self, options):
1779 """Processes the --swarming option and aborts if not specified.
1780
1781 Returns the identity as determined by the server.
1782 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001783 if not options.swarming:
1784 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001785 try:
1786 options.swarming = net.fix_url(options.swarming)
1787 except ValueError as e:
1788 self.error('--swarming %s' % e)
1789 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001790 try:
1791 user = auth.ensure_logged_in(options.swarming)
1792 except ValueError as e:
1793 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001794 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001795
1796
1797def main(args):
1798 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001799 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001800
1801
1802if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001803 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +00001804 fix_encoding.fix_encoding()
1805 tools.disable_buffering()
1806 colorama.init()
1807 sys.exit(main(sys.argv[1:]))