blob: bf4eb59f5f194e8674f968402fa0f610e4c20b48 [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
Takuto Ikuta0e3e1c42018-11-29 14:21:06 +00008__version__ = '0.14'
maruel@chromium.org0437a732013-08-27 16:05:52 +00009
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050010import collections
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -040011import datetime
maruel@chromium.org0437a732013-08-27 16:05:52 +000012import json
13import logging
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040014import optparse
maruel@chromium.org0437a732013-08-27 16:05:52 +000015import os
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +100016import re
maruel@chromium.org0437a732013-08-27 16:05:52 +000017import sys
maruel11e31af2017-02-15 07:30:50 -080018import textwrap
Vadim Shtayurab19319e2014-04-27 08:50:06 -070019import threading
maruel@chromium.org0437a732013-08-27 16:05:52 +000020import time
21import urllib
maruel@chromium.org0437a732013-08-27 16:05:52 +000022
23from third_party import colorama
24from third_party.depot_tools import fix_encoding
25from third_party.depot_tools import subcommand
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000026
Marc-Antoine Ruel8806e622014-02-12 14:15:53 -050027from utils import file_path
maruel12e30012015-10-09 11:55:35 -070028from utils import fs
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040029from utils import logging_utils
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -040030from third_party.chromium import natsort
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000031from utils import net
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -040032from utils import on_error
maruel8e4e40c2016-05-30 06:21:07 -070033from utils import subprocess42
maruel@chromium.org0437a732013-08-27 16:05:52 +000034from utils import threading_utils
vadimsh@chromium.org6b706212013-08-28 15:03:46 +000035from utils import tools
maruel@chromium.org0437a732013-08-27 16:05:52 +000036
Vadim Shtayurae34e13a2014-02-02 11:23:26 -080037import auth
iannucci31ab9192017-05-02 19:11:56 -070038import cipd
maruel@chromium.org7b844a62013-09-17 13:04:59 +000039import isolateserver
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +000040import isolate_storage
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +000041import isolated_format
Marc-Antoine Ruel2666d9c2018-05-18 13:52:02 -040042import local_caching
maruelc070e672016-02-22 17:32:57 -080043import run_isolated
maruel@chromium.org0437a732013-08-27 16:05:52 +000044
45
tansella4949442016-06-23 22:34:32 -070046ROOT_DIR = os.path.dirname(os.path.abspath(
47 __file__.decode(sys.getfilesystemencoding())))
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050048
49
50class Failure(Exception):
51 """Generic failure."""
52 pass
53
54
maruel0a25f6c2017-05-10 10:43:23 -070055def default_task_name(options):
56 """Returns a default task name if not specified."""
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050057 if not options.task_name:
maruel0a25f6c2017-05-10 10:43:23 -070058 task_name = u'%s/%s' % (
marueld9cc8422017-05-09 12:07:02 -070059 options.user,
maruelaf6b06c2017-06-08 06:26:53 -070060 '_'.join('%s=%s' % (k, v) for k, v in options.dimensions))
maruel0a25f6c2017-05-10 10:43:23 -070061 if options.isolated:
62 task_name += u'/' + options.isolated
63 return task_name
64 return options.task_name
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -050065
66
67### Triggering.
68
69
maruel77f720b2015-09-15 12:35:22 -070070# See ../appengine/swarming/swarming_rpcs.py.
borenet02f772b2016-06-22 12:42:19 -070071CipdPackage = collections.namedtuple(
72 'CipdPackage',
73 [
74 'package_name',
75 'path',
76 'version',
77 ])
78
79
80# See ../appengine/swarming/swarming_rpcs.py.
81CipdInput = collections.namedtuple(
82 'CipdInput',
83 [
84 'client_package',
85 'packages',
86 'server',
87 ])
88
89
90# See ../appengine/swarming/swarming_rpcs.py.
maruel77f720b2015-09-15 12:35:22 -070091FilesRef = collections.namedtuple(
92 'FilesRef',
93 [
94 'isolated',
95 'isolatedserver',
96 'namespace',
97 ])
98
99
100# See ../appengine/swarming/swarming_rpcs.py.
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800101StringListPair = collections.namedtuple(
102 'StringListPair', [
103 'key',
104 'value', # repeated string
105 ]
106)
107
108
109# See ../appengine/swarming/swarming_rpcs.py.
maruel77f720b2015-09-15 12:35:22 -0700110TaskProperties = collections.namedtuple(
111 'TaskProperties',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500112 [
maruel681d6802017-01-17 16:56:03 -0800113 'caches',
borenet02f772b2016-06-22 12:42:19 -0700114 'cipd_input',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500115 'command',
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -0500116 'relative_cwd',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500117 'dimensions',
118 'env',
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800119 'env_prefixes',
maruel77f720b2015-09-15 12:35:22 -0700120 'execution_timeout_secs',
121 'extra_args',
122 'grace_period_secs',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500123 'idempotent',
maruel77f720b2015-09-15 12:35:22 -0700124 'inputs_ref',
125 'io_timeout_secs',
aludwincc5524e2016-10-28 10:25:24 -0700126 'outputs',
iannuccidc80dfb2016-10-28 12:50:20 -0700127 'secret_bytes',
maruel77f720b2015-09-15 12:35:22 -0700128 ])
129
130
131# See ../appengine/swarming/swarming_rpcs.py.
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -0400132TaskSlice = collections.namedtuple(
133 'TaskSlice',
134 [
135 'expiration_secs',
136 'properties',
137 'wait_for_capacity',
138 ])
139
140
141# See ../appengine/swarming/swarming_rpcs.py.
maruel77f720b2015-09-15 12:35:22 -0700142NewTaskRequest = collections.namedtuple(
143 'NewTaskRequest',
144 [
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500145 'name',
maruel77f720b2015-09-15 12:35:22 -0700146 'parent_task_id',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500147 'priority',
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400148 'task_slices',
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700149 'service_account',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500150 'tags',
151 'user',
Robert Iannuccifafa7352018-06-13 17:08:17 +0000152 'pool_task_template',
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500153 ])
154
155
maruel77f720b2015-09-15 12:35:22 -0700156def namedtuple_to_dict(value):
157 """Recursively converts a namedtuple to a dict."""
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -0400158 if hasattr(value, '_asdict'):
159 return namedtuple_to_dict(value._asdict())
160 if isinstance(value, (list, tuple)):
161 return [namedtuple_to_dict(v) for v in value]
162 if isinstance(value, dict):
163 return {k: namedtuple_to_dict(v) for k, v in value.iteritems()}
164 return value
maruel77f720b2015-09-15 12:35:22 -0700165
166
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700167def task_request_to_raw_request(task_request):
maruel71c61c82016-02-22 06:52:05 -0800168 """Returns the json-compatible dict expected by the server for new request.
maruelaf6269b2015-09-10 14:37:51 -0700169
170 This is for the v1 client Swarming API.
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500171 """
maruel77f720b2015-09-15 12:35:22 -0700172 out = namedtuple_to_dict(task_request)
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700173 # Don't send 'service_account' if it is None to avoid confusing older
174 # version of the server that doesn't know about 'service_account' and don't
175 # use it at all.
176 if not out['service_account']:
177 out.pop('service_account')
Brad Hallf78187a2018-10-19 17:08:55 +0000178 for task_slice in out['task_slices']:
179 task_slice['properties']['env'] = [
180 {'key': k, 'value': v}
181 for k, v in task_slice['properties']['env'].iteritems()
182 ]
183 task_slice['properties']['env'].sort(key=lambda x: x['key'])
maruel77f720b2015-09-15 12:35:22 -0700184 return out
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500185
186
maruel77f720b2015-09-15 12:35:22 -0700187def swarming_trigger(swarming, raw_request):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500188 """Triggers a request on the Swarming server and returns the json data.
189
190 It's the low-level function.
191
192 Returns:
193 {
194 'request': {
195 'created_ts': u'2010-01-02 03:04:05',
196 'name': ..
197 },
198 'task_id': '12300',
199 }
200 """
201 logging.info('Triggering: %s', raw_request['name'])
202
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500203 result = net.url_read_json(
smut281c3902018-05-30 17:50:05 -0700204 swarming + '/_ah/api/swarming/v1/tasks/new', data=raw_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500205 if not result:
206 on_error.report('Failed to trigger task %s' % raw_request['name'])
207 return None
maruele557bce2015-11-17 09:01:27 -0800208 if result.get('error'):
marueld4d15312015-11-16 17:22:59 -0800209 # The reply is an error.
maruele557bce2015-11-17 09:01:27 -0800210 msg = 'Failed to trigger task %s' % raw_request['name']
211 if result['error'].get('errors'):
212 for err in result['error']['errors']:
213 if err.get('message'):
214 msg += '\nMessage: %s' % err['message']
215 if err.get('debugInfo'):
216 msg += '\nDebug info:\n%s' % err['debugInfo']
217 elif result['error'].get('message'):
maruelbf53e042015-12-01 15:00:51 -0800218 msg += '\nMessage: %s' % result['error']['message']
maruele557bce2015-11-17 09:01:27 -0800219
220 on_error.report(msg)
marueld4d15312015-11-16 17:22:59 -0800221 return None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500222 return result
223
224
225def setup_googletest(env, shards, index):
226 """Sets googletest specific environment variables."""
227 if shards > 1:
maruel77f720b2015-09-15 12:35:22 -0700228 assert not any(i['key'] == 'GTEST_SHARD_INDEX' for i in env), env
229 assert not any(i['key'] == 'GTEST_TOTAL_SHARDS' for i in env), env
230 env = env[:]
231 env.append({'key': 'GTEST_SHARD_INDEX', 'value': str(index)})
232 env.append({'key': 'GTEST_TOTAL_SHARDS', 'value': str(shards)})
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500233 return env
234
235
236def trigger_task_shards(swarming, task_request, shards):
237 """Triggers one or many subtasks of a sharded task.
238
239 Returns:
240 Dict with task details, returned to caller as part of --dump-json output.
241 None in case of failure.
242 """
243 def convert(index):
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700244 req = task_request_to_raw_request(task_request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500245 if shards > 1:
Brad Hall157bec82018-11-26 22:15:38 +0000246 for task_slice in req['task_slices']:
247 task_slice['properties']['env'] = setup_googletest(
248 task_slice['properties']['env'], shards, index)
maruel77f720b2015-09-15 12:35:22 -0700249 req['name'] += ':%s:%s' % (index, shards)
250 return req
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500251
252 requests = [convert(index) for index in xrange(shards)]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500253 tasks = {}
254 priority_warning = False
255 for index, request in enumerate(requests):
maruel77f720b2015-09-15 12:35:22 -0700256 task = swarming_trigger(swarming, request)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500257 if not task:
258 break
259 logging.info('Request result: %s', task)
260 if (not priority_warning and
Marc-Antoine Ruelb1216762017-08-17 10:07:49 -0400261 int(task['request']['priority']) != task_request.priority):
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500262 priority_warning = True
263 print >> sys.stderr, (
264 'Priority was reset to %s' % task['request']['priority'])
265 tasks[request['name']] = {
266 'shard_index': index,
267 'task_id': task['task_id'],
268 'view_url': '%s/user/task/%s' % (swarming, task['task_id']),
269 }
270
271 # Some shards weren't triggered. Abort everything.
272 if len(tasks) != len(requests):
273 if tasks:
274 print >> sys.stderr, 'Only %d shard(s) out of %d were triggered' % (
275 len(tasks), len(requests))
276 for task_dict in tasks.itervalues():
277 abort_task(swarming, task_dict['task_id'])
278 return None
279
280 return tasks
281
282
283### Collection.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000284
285
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700286# How often to print status updates to stdout in 'collect'.
Jao-ke Chin-Leeba184e62018-11-19 17:04:41 +0000287STATUS_UPDATE_INTERVAL = 5 * 60.
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700288
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400289
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000290class TaskState(object):
291 """Represents the current task state.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000292
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000293 For documentation, see the comments in the swarming_rpcs.TaskState enum, which
294 is the source of truth for these values:
295 https://cs.chromium.org/chromium/infra/luci/appengine/swarming/swarming_rpcs.py?q=TaskState\(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400296
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000297 It's in fact an enum.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400298 """
299 RUNNING = 0x10
300 PENDING = 0x20
301 EXPIRED = 0x30
302 TIMED_OUT = 0x40
303 BOT_DIED = 0x50
304 CANCELED = 0x60
305 COMPLETED = 0x70
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400306 KILLED = 0x80
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400307 NO_RESOURCE = 0x100
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400308
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000309 STATES_RUNNING = ('PENDING', 'RUNNING')
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400310
maruel77f720b2015-09-15 12:35:22 -0700311 _ENUMS = {
312 'RUNNING': RUNNING,
313 'PENDING': PENDING,
314 'EXPIRED': EXPIRED,
315 'TIMED_OUT': TIMED_OUT,
316 'BOT_DIED': BOT_DIED,
317 'CANCELED': CANCELED,
318 'COMPLETED': COMPLETED,
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400319 'KILLED': KILLED,
Marc-Antoine Ruelfc708352018-05-04 20:25:43 -0400320 'NO_RESOURCE': NO_RESOURCE,
maruel77f720b2015-09-15 12:35:22 -0700321 }
322
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400323 @classmethod
maruel77f720b2015-09-15 12:35:22 -0700324 def from_enum(cls, state):
325 """Returns int value based on the string."""
326 if state not in cls._ENUMS:
327 raise ValueError('Invalid state %s' % state)
328 return cls._ENUMS[state]
329
maruel@chromium.org0437a732013-08-27 16:05:52 +0000330
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700331class TaskOutputCollector(object):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700332 """Assembles task execution summary (for --task-summary-json output).
333
334 Optionally fetches task outputs from isolate server to local disk (used when
335 --task-output-dir is passed).
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700336
337 This object is shared among multiple threads running 'retrieve_results'
338 function, in particular they call 'process_shard_result' method in parallel.
339 """
340
Takuto Ikuta1e6072c2018-11-06 20:42:43 +0000341 def __init__(self, task_output_dir, task_output_stdout, shard_count,
342 filter_cb):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700343 """Initializes TaskOutputCollector, ensures |task_output_dir| exists.
344
345 Args:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700346 task_output_dir: (optional) local directory to put fetched files to.
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700347 shard_count: expected number of task shards.
348 """
maruel12e30012015-10-09 11:55:35 -0700349 self.task_output_dir = (
350 unicode(os.path.abspath(task_output_dir))
351 if task_output_dir else task_output_dir)
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000352 self.task_output_stdout = task_output_stdout
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700353 self.shard_count = shard_count
Takuto Ikuta1e6072c2018-11-06 20:42:43 +0000354 self.filter_cb = filter_cb
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700355
356 self._lock = threading.Lock()
357 self._per_shard_results = {}
358 self._storage = None
359
nodire5028a92016-04-29 14:38:21 -0700360 if self.task_output_dir:
361 file_path.ensure_tree(self.task_output_dir)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700362
Vadim Shtayurab450c602014-05-12 19:23:25 -0700363 def process_shard_result(self, shard_index, result):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700364 """Stores results of a single task shard, fetches output files if necessary.
365
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400366 Modifies |result| in place.
367
maruel77f720b2015-09-15 12:35:22 -0700368 shard_index is 0-based.
369
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700370 Called concurrently from multiple threads.
371 """
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700372 # Sanity check index is in expected range.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700373 assert isinstance(shard_index, int)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700374 if shard_index < 0 or shard_index >= self.shard_count:
375 logging.warning(
376 'Shard index %d is outside of expected range: [0; %d]',
377 shard_index, self.shard_count - 1)
378 return
379
maruel77f720b2015-09-15 12:35:22 -0700380 if result.get('outputs_ref'):
381 ref = result['outputs_ref']
382 result['outputs_ref']['view_url'] = '%s/browse?%s' % (
383 ref['isolatedserver'],
384 urllib.urlencode(
385 [('namespace', ref['namespace']), ('hash', ref['isolated'])]))
Kevin Graneyc2c3b9e2014-08-26 09:04:17 -0400386
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700387 # Store result dict of that shard, ignore results we've already seen.
388 with self._lock:
389 if shard_index in self._per_shard_results:
390 logging.warning('Ignoring duplicate shard index %d', shard_index)
391 return
392 self._per_shard_results[shard_index] = result
393
394 # Fetch output files if necessary.
maruel77f720b2015-09-15 12:35:22 -0700395 if self.task_output_dir and result.get('outputs_ref'):
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000396 server_ref = isolate_storage.ServerRef(
397 result['outputs_ref']['isolatedserver'],
398 result['outputs_ref']['namespace'])
399 storage = self._get_storage(server_ref)
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400400 if storage:
401 # Output files are supposed to be small and they are not reused across
Marc-Antoine Ruel2666d9c2018-05-18 13:52:02 -0400402 # tasks. So use MemoryContentAddressedCache for them instead of on-disk
403 # cache. Make files writable, so that calling script can delete them.
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400404 isolateserver.fetch_isolated(
maruel77f720b2015-09-15 12:35:22 -0700405 result['outputs_ref']['isolated'],
Marc-Antoine Ruele4dcbb82014-10-01 09:30:56 -0400406 storage,
Marc-Antoine Ruel2666d9c2018-05-18 13:52:02 -0400407 local_caching.MemoryContentAddressedCache(file_mode_mask=0700),
maruel4409e302016-07-19 14:25:51 -0700408 os.path.join(self.task_output_dir, str(shard_index)),
Takuto Ikuta1e6072c2018-11-06 20:42:43 +0000409 False, self.filter_cb)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700410
411 def finalize(self):
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700412 """Assembles and returns task summary JSON, shutdowns underlying Storage."""
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700413 with self._lock:
414 # Write an array of shard results with None for missing shards.
415 summary = {
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700416 'shards': [
417 self._per_shard_results.get(i) for i in xrange(self.shard_count)
418 ],
419 }
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000420
421 # Don't store stdout in the summary if not requested too.
422 if "json" not in self.task_output_stdout:
423 for shard_json in summary['shards']:
424 if not shard_json:
425 continue
426 if "output" in shard_json:
427 del shard_json["output"]
428 if "outputs" in shard_json:
429 del shard_json["outputs"]
430
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700431 # Write summary.json to task_output_dir as well.
432 if self.task_output_dir:
433 tools.write_json(
maruel12e30012015-10-09 11:55:35 -0700434 os.path.join(self.task_output_dir, u'summary.json'),
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700435 summary,
436 False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700437 if self._storage:
438 self._storage.close()
439 self._storage = None
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700440 return summary
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700441
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000442 def _get_storage(self, server_ref):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700443 """Returns isolateserver.Storage to use to fetch files."""
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700444 assert self.task_output_dir
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700445 with self._lock:
446 if not self._storage:
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000447 self._storage = isolateserver.get_storage(server_ref)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700448 else:
449 # Shards must all use exact same isolate server and namespace.
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000450 if self._storage.server_ref.url != server_ref.url:
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700451 logging.error(
452 'Task shards are using multiple isolate servers: %s and %s',
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000453 self._storage.server_ref.url, server_ref.url)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700454 return None
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000455 if self._storage.server_ref.namespace != server_ref.namespace:
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700456 logging.error(
457 'Task shards are using multiple namespaces: %s and %s',
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +0000458 self._storage.server_ref.namespace, server_ref.namespace)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700459 return None
460 return self._storage
461
462
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500463def now():
464 """Exists so it can be mocked easily."""
465 return time.time()
466
467
maruel77f720b2015-09-15 12:35:22 -0700468def parse_time(value):
469 """Converts serialized time from the API to datetime.datetime."""
470 # When microseconds are 0, the '.123456' suffix is elided. This means the
471 # serialized format is not consistent, which confuses the hell out of python.
472 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'):
473 try:
474 return datetime.datetime.strptime(value, fmt)
475 except ValueError:
476 pass
477 raise ValueError('Failed to parse %s' % value)
478
479
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700480def retrieve_results(
maruel9531ce02016-04-13 06:11:23 -0700481 base_url, shard_index, task_id, timeout, should_stop, output_collector,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000482 include_perf, fetch_stdout):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400483 """Retrieves results for a single task ID.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700484
Vadim Shtayurab450c602014-05-12 19:23:25 -0700485 Returns:
486 <result dict> on success.
487 None on failure.
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700488 """
maruel71c61c82016-02-22 06:52:05 -0800489 assert timeout is None or isinstance(timeout, float), timeout
smut281c3902018-05-30 17:50:05 -0700490 result_url = '%s/_ah/api/swarming/v1/task/%s/result' % (base_url, task_id)
maruel9531ce02016-04-13 06:11:23 -0700491 if include_perf:
492 result_url += '?include_performance_stats=true'
smut281c3902018-05-30 17:50:05 -0700493 output_url = '%s/_ah/api/swarming/v1/task/%s/stdout' % (base_url, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700494 started = now()
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400495 deadline = started + timeout if timeout > 0 else None
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700496 attempt = 0
497
498 while not should_stop.is_set():
499 attempt += 1
500
501 # Waiting for too long -> give up.
502 current_time = now()
503 if deadline and current_time >= deadline:
504 logging.error('retrieve_results(%s) timed out on attempt %d',
505 base_url, attempt)
506 return None
507
508 # Do not spin too fast. Spin faster at the beginning though.
509 # Start with 1 sec delay and for each 30 sec of waiting add another second
510 # of delay, until hitting 15 sec ceiling.
511 if attempt > 1:
512 max_delay = min(15, 1 + (current_time - started) / 30.0)
513 delay = min(max_delay, deadline - current_time) if deadline else max_delay
514 if delay > 0:
515 logging.debug('Waiting %.1f sec before retrying', delay)
516 should_stop.wait(delay)
517 if should_stop.is_set():
518 return None
519
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400520 # Disable internal retries in net.url_read_json, since we are doing retries
521 # ourselves.
522 # TODO(maruel): We'd need to know if it's a 404 and not retry at all.
maruel0eb1d1b2015-10-02 14:48:21 -0700523 # TODO(maruel): Sadly, we currently have to poll here. Use hanging HTTP
524 # request on GAE v2.
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400525 # Retry on 500s only if no timeout is specified.
526 result = net.url_read_json(result_url, retry_50x=bool(timeout == -1))
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400527 if not result:
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400528 if timeout == -1:
529 return None
Marc-Antoine Ruel200b3952014-08-14 11:07:44 -0400530 continue
maruel77f720b2015-09-15 12:35:22 -0700531
maruelbf53e042015-12-01 15:00:51 -0800532 if result.get('error'):
533 # An error occurred.
534 if result['error'].get('errors'):
535 for err in result['error']['errors']:
536 logging.warning(
537 'Error while reading task: %s; %s',
538 err.get('message'), err.get('debugInfo'))
539 elif result['error'].get('message'):
540 logging.warning(
541 'Error while reading task: %s', result['error']['message'])
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400542 if timeout == -1:
543 return result
maruelbf53e042015-12-01 15:00:51 -0800544 continue
545
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -0400546 # When timeout == -1, always return on first attempt. 500s are already
547 # retried in this case.
Marc-Antoine Ruel20b764d2018-06-22 18:08:37 +0000548 if result['state'] not in TaskState.STATES_RUNNING or timeout == -1:
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000549 if fetch_stdout:
550 out = net.url_read_json(output_url)
Vadim Shtayura6fd3c7b2017-11-03 15:32:51 -0700551 result['output'] = out.get('output', '') if out else ''
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700552 # Record the result, try to fetch attached output files (if any).
553 if output_collector:
554 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching.
Vadim Shtayurab450c602014-05-12 19:23:25 -0700555 output_collector.process_shard_result(shard_index, result)
maruel77f720b2015-09-15 12:35:22 -0700556 if result.get('internal_failure'):
557 logging.error('Internal error!')
558 elif result['state'] == 'BOT_DIED':
559 logging.error('Bot died!')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700560 return result
maruel@chromium.org0437a732013-08-27 16:05:52 +0000561
562
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700563def yield_results(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400564 swarm_base_url, task_ids, timeout, max_threads, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000565 output_collector, include_perf, fetch_stdout):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500566 """Yields swarming task results from the swarming server as (index, result).
maruel@chromium.org0437a732013-08-27 16:05:52 +0000567
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700568 Duplicate shards are ignored. Shards are yielded in order of completion.
569 Timed out shards are NOT yielded at all. Caller can compare number of yielded
570 shards with len(task_keys) to verify all shards completed.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000571
572 max_threads is optional and is used to limit the number of parallel fetches
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500573 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 +0000574 worth normally to limit the number threads. Mostly used for testing purposes.
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500575
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700576 output_collector is an optional instance of TaskOutputCollector that will be
577 used to fetch files produced by a task from isolate server to the local disk.
578
Marc-Antoine Ruel5c720342014-02-21 14:46:14 -0500579 Yields:
580 (index, result). In particular, 'result' is defined as the
581 GetRunnerResults() function in services/swarming/server/test_runner.py.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000582 """
maruel@chromium.org0437a732013-08-27 16:05:52 +0000583 number_threads = (
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400584 min(max_threads, len(task_ids)) if max_threads else len(task_ids))
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700585 should_stop = threading.Event()
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700586 results_channel = threading_utils.TaskChannel()
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700587
maruel@chromium.org0437a732013-08-27 16:05:52 +0000588 with threading_utils.ThreadPool(number_threads, number_threads, 0) as pool:
589 try:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700590 # Adds a task to the thread pool to call 'retrieve_results' and return
591 # the results together with shard_index that produced them (as a tuple).
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400592 def enqueue_retrieve_results(shard_index, task_id):
Marc-Antoine Ruel486c9b52018-07-23 19:30:47 +0000593 # pylint: disable=no-value-for-parameter
Vadim Shtayurab450c602014-05-12 19:23:25 -0700594 task_fn = lambda *args: (shard_index, retrieve_results(*args))
maruel@chromium.org0437a732013-08-27 16:05:52 +0000595 pool.add_task(
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400596 0, results_channel.wrap_task(task_fn), swarm_base_url, shard_index,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000597 task_id, timeout, should_stop, output_collector, include_perf,
598 fetch_stdout)
Vadim Shtayurab450c602014-05-12 19:23:25 -0700599
600 # Enqueue 'retrieve_results' calls for each shard key to run in parallel.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400601 for shard_index, task_id in enumerate(task_ids):
602 enqueue_retrieve_results(shard_index, task_id)
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700603
604 # Wait for all of them to finish.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400605 shards_remaining = range(len(task_ids))
606 active_task_count = len(task_ids)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700607 while active_task_count:
Vadim Shtayurab450c602014-05-12 19:23:25 -0700608 shard_index, result = None, None
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700609 try:
Marc-Antoine Ruel4494b6c2018-11-28 21:00:41 +0000610 shard_index, result = results_channel.next(
Vadim Shtayurab450c602014-05-12 19:23:25 -0700611 timeout=STATUS_UPDATE_INTERVAL)
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700612 except threading_utils.TaskChannel.Timeout:
613 if print_status_updates:
Jao-ke Chin-Leeba184e62018-11-19 17:04:41 +0000614 time_now = str(datetime.datetime.now())
615 _, time_now = time_now.split(' ')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700616 print(
Jao-ke Chin-Leeba184e62018-11-19 17:04:41 +0000617 '%s '
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700618 'Waiting for results from the following shards: %s' %
Jao-ke Chin-Leeba184e62018-11-19 17:04:41 +0000619 (time_now, ', '.join(map(str, shards_remaining)))
620 )
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700621 sys.stdout.flush()
622 continue
623 except Exception:
624 logging.exception('Unexpected exception in retrieve_results')
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700625
626 # A call to 'retrieve_results' finished (successfully or not).
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700627 active_task_count -= 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000628 if not result:
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -0500629 logging.error('Failed to retrieve the results for a swarming key')
maruel@chromium.org0437a732013-08-27 16:05:52 +0000630 continue
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700631
Vadim Shtayurab450c602014-05-12 19:23:25 -0700632 # Yield back results to the caller.
633 assert shard_index in shards_remaining
634 shards_remaining.remove(shard_index)
635 yield shard_index, result
Vadim Shtayurab19319e2014-04-27 08:50:06 -0700636
maruel@chromium.org0437a732013-08-27 16:05:52 +0000637 finally:
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700638 # Done or aborted with Ctrl+C, kill the remaining threads.
maruel@chromium.org0437a732013-08-27 16:05:52 +0000639 should_stop.set()
640
641
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000642def decorate_shard_output(swarming, shard_index, metadata, include_stdout):
maruel@chromium.org0437a732013-08-27 16:05:52 +0000643 """Returns wrapped output for swarming task shard."""
maruel77f720b2015-09-15 12:35:22 -0700644 if metadata.get('started_ts') and not metadata.get('deduped_from'):
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400645 pending = '%.1fs' % (
maruel77f720b2015-09-15 12:35:22 -0700646 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts'])
647 ).total_seconds()
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400648 elif (metadata.get('state') in ('BOT_DIED', 'CANCELED', 'EXPIRED') and
649 metadata.get('abandoned_ts')):
650 pending = '%.1fs' % (
651 parse_time(metadata['abandoned_ts']) -
652 parse_time(metadata['created_ts'])
653 ).total_seconds()
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400654 else:
655 pending = 'N/A'
656
maruel77f720b2015-09-15 12:35:22 -0700657 if metadata.get('duration') is not None:
658 duration = '%.1fs' % metadata['duration']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400659 else:
660 duration = 'N/A'
661
maruel77f720b2015-09-15 12:35:22 -0700662 if metadata.get('exit_code') is not None:
663 # Integers are encoded as string to not loose precision.
664 exit_code = '%s' % metadata['exit_code']
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400665 else:
666 exit_code = 'N/A'
667
668 bot_id = metadata.get('bot_id') or 'N/A'
669
maruel77f720b2015-09-15 12:35:22 -0700670 url = '%s/user/task/%s' % (swarming, metadata['task_id'])
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400671 tag_header = 'Shard %d %s' % (shard_index, url)
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000672 tag_footer1 = 'End of shard %d' % (shard_index)
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400673 if metadata.get('state') == 'CANCELED':
674 tag_footer2 = ' Pending: %s CANCELED' % pending
675 elif metadata.get('state') == 'EXPIRED':
676 tag_footer2 = ' Pending: %s EXPIRED (lack of capacity)' % pending
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -0400677 elif metadata.get('state') in ('BOT_DIED', 'TIMED_OUT', 'KILLED'):
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400678 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s %s' % (
679 pending, duration, bot_id, exit_code, metadata['state'])
680 else:
681 tag_footer2 = ' Pending: %s Duration: %s Bot: %s Exit: %s' % (
682 pending, duration, bot_id, exit_code)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400683
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000684 tag_len = max(len(x) for x in [tag_header, tag_footer1, tag_footer2])
685 dash_pad = '+-%s-+' % ('-' * tag_len)
686 tag_header = '| %s |' % tag_header.ljust(tag_len)
687 tag_footer1 = '| %s |' % tag_footer1.ljust(tag_len)
688 tag_footer2 = '| %s |' % tag_footer2.ljust(tag_len)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400689
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000690 if include_stdout:
691 return '\n'.join([
692 dash_pad,
693 tag_header,
694 dash_pad,
Marc-Antoine Ruel3f9931a2017-11-03 14:34:49 -0400695 (metadata.get('output') or '').rstrip(),
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000696 dash_pad,
697 tag_footer1,
698 tag_footer2,
699 dash_pad,
700 ])
701 else:
702 return '\n'.join([
703 dash_pad,
704 tag_header,
705 tag_footer2,
706 dash_pad,
707 ])
maruel@chromium.org0437a732013-08-27 16:05:52 +0000708
709
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700710def collect(
maruel0eb1d1b2015-10-02 14:48:21 -0700711 swarming, task_ids, timeout, decorate, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000712 task_summary_json, task_output_dir, task_output_stdout,
Takuto Ikuta1e6072c2018-11-06 20:42:43 +0000713 include_perf, filepath_filter):
maruela5490782015-09-30 10:56:59 -0700714 """Retrieves results of a Swarming task.
715
716 Returns:
717 process exit code that should be returned to the user.
718 """
Takuto Ikuta1e6072c2018-11-06 20:42:43 +0000719
720 filter_cb = None
721 if filepath_filter:
722 filter_cb = re.compile(filepath_filter).match
723
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700724 # Collect summary JSON and output files (if task_output_dir is not None).
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000725 output_collector = TaskOutputCollector(
Takuto Ikuta1e6072c2018-11-06 20:42:43 +0000726 task_output_dir, task_output_stdout, len(task_ids), filter_cb)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700727
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700728 seen_shards = set()
maruela5490782015-09-30 10:56:59 -0700729 exit_code = None
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400730 total_duration = 0
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700731 try:
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400732 for index, metadata in yield_results(
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400733 swarming, task_ids, timeout, None, print_status_updates,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000734 output_collector, include_perf,
735 (len(task_output_stdout) > 0),
736 ):
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700737 seen_shards.add(index)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700738
Marc-Antoine Ruel5e6ccdb2015-04-02 15:55:13 -0400739 # Default to failure if there was no process that even started.
maruel77f720b2015-09-15 12:35:22 -0700740 shard_exit_code = metadata.get('exit_code')
741 if shard_exit_code:
maruela5490782015-09-30 10:56:59 -0700742 # It's encoded as a string, so bool('0') is True.
maruel77f720b2015-09-15 12:35:22 -0700743 shard_exit_code = int(shard_exit_code)
maruela5490782015-09-30 10:56:59 -0700744 if shard_exit_code or exit_code is None:
Marc-Antoine Ruel4e6b73d2014-10-03 18:00:05 -0400745 exit_code = shard_exit_code
maruel77f720b2015-09-15 12:35:22 -0700746 total_duration += metadata.get('duration', 0)
Vadim Shtayura473455a2014-05-14 15:22:35 -0700747
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700748 if decorate:
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000749 s = decorate_shard_output(
750 swarming, index, metadata,
751 "console" in task_output_stdout).encode(
752 'utf-8', 'replace')
leileied181762016-10-13 14:24:59 -0700753 print(s)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400754 if len(seen_shards) < len(task_ids):
755 print('')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700756 else:
maruel77f720b2015-09-15 12:35:22 -0700757 print('%s: %s %s' % (
758 metadata.get('bot_id', 'N/A'),
759 metadata['task_id'],
760 shard_exit_code))
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +1000761 if "console" in task_output_stdout and metadata['output']:
maruel77f720b2015-09-15 12:35:22 -0700762 output = metadata['output'].rstrip()
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400763 if output:
764 print(''.join(' %s\n' % l for l in output.splitlines()))
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700765 finally:
Vadim Shtayurac8437bf2014-07-09 19:45:36 -0700766 summary = output_collector.finalize()
767 if task_summary_json:
768 tools.write_json(task_summary_json, summary, False)
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700769
Marc-Antoine Rueld59e8072014-10-21 18:54:45 -0400770 if decorate and total_duration:
771 print('Total duration: %.1fs' % total_duration)
772
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -0400773 if len(seen_shards) != len(task_ids):
774 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards]
Vadim Shtayura86a2cef2014-04-18 11:13:39 -0700775 print >> sys.stderr, ('Results from some shards are missing: %s' %
776 ', '.join(map(str, missing_shards)))
Vadim Shtayurac524f512014-05-15 09:54:56 -0700777 return 1
Vadim Shtayurae3fbd102014-04-29 17:05:21 -0700778
maruela5490782015-09-30 10:56:59 -0700779 return exit_code if exit_code is not None else 1
maruel@chromium.org0437a732013-08-27 16:05:52 +0000780
781
maruel77f720b2015-09-15 12:35:22 -0700782### API management.
783
784
785class APIError(Exception):
786 pass
787
788
789def endpoints_api_discovery_apis(host):
790 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all
791 the APIs exposed by a host.
792
793 https://developers.google.com/discovery/v1/reference/apis/list
794 """
maruel380e3262016-08-31 16:10:06 -0700795 # Uses the real Cloud Endpoints. This needs to be fixed once the Cloud
796 # Endpoints version is turned down.
maruel77f720b2015-09-15 12:35:22 -0700797 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis')
798 if data is None:
799 raise APIError('Failed to discover APIs on %s' % host)
800 out = {}
801 for api in data['items']:
802 if api['id'] == 'discovery:v1':
803 continue
804 # URL is of the following form:
805 # url = host + (
806 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version'])
807 api_data = net.url_read_json(api['discoveryRestUrl'])
808 if api_data is None:
809 raise APIError('Failed to discover %s on %s' % (api['id'], host))
810 out[api['id']] = api_data
811 return out
812
813
maruelaf6b06c2017-06-08 06:26:53 -0700814def get_yielder(base_url, limit):
815 """Returns the first query and a function that yields following items."""
816 CHUNK_SIZE = 250
817
818 url = base_url
819 if limit:
820 url += '%slimit=%d' % ('&' if '?' in url else '?', min(CHUNK_SIZE, limit))
821 data = net.url_read_json(url)
822 if data is None:
823 # TODO(maruel): Do basic diagnostic.
824 raise Failure('Failed to access %s' % url)
825 org_cursor = data.pop('cursor', None)
826 org_total = len(data.get('items') or [])
827 logging.info('get_yielder(%s) returning %d items', base_url, org_total)
828 if not org_cursor or not org_total:
829 # This is not an iterable resource.
830 return data, lambda: []
831
832 def yielder():
833 cursor = org_cursor
834 total = org_total
835 # Some items support cursors. Try to get automatically if cursors are needed
836 # by looking at the 'cursor' items.
837 while cursor and (not limit or total < limit):
838 merge_char = '&' if '?' in base_url else '?'
839 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(cursor))
840 if limit:
841 url += '&limit=%d' % min(CHUNK_SIZE, limit - total)
842 new = net.url_read_json(url)
843 if new is None:
844 raise Failure('Failed to access %s' % url)
845 cursor = new.get('cursor')
846 new_items = new.get('items')
847 nb_items = len(new_items or [])
848 total += nb_items
849 logging.info('get_yielder(%s) yielding %d items', base_url, nb_items)
850 yield new_items
851
852 return data, yielder
853
854
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500855### Commands.
856
857
858def abort_task(_swarming, _manifest):
859 """Given a task manifest that was triggered, aborts its execution."""
860 # TODO(vadimsh): No supported by the server yet.
861
862
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400863def add_filter_options(parser):
maruel681d6802017-01-17 16:56:03 -0800864 parser.filter_group = optparse.OptionGroup(parser, 'Bot selection')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500865 parser.filter_group.add_option(
Marc-Antoine Ruelb39e8cf2014-01-20 10:39:31 -0500866 '-d', '--dimension', default=[], action='append', nargs=2,
Marc-Antoine Ruel92f32422013-11-06 18:12:13 -0500867 dest='dimensions', metavar='FOO bar',
868 help='dimension to filter on')
Brad Hallf78187a2018-10-19 17:08:55 +0000869 parser.filter_group.add_option(
870 '--optional-dimension', default=[], action='append', nargs=3,
871 dest='optional_dimensions', metavar='key value expiration',
872 help='optional dimensions which will result in additional task slices ')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500873 parser.add_option_group(parser.filter_group)
874
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400875
Brad Hallf78187a2018-10-19 17:08:55 +0000876def _validate_filter_option(parser, key, value, expiration, argname):
877 if ':' in key:
878 parser.error('%s key cannot contain ":"' % argname)
879 if key.strip() != key:
880 parser.error('%s key has whitespace' % argname)
881 if not key:
882 parser.error('%s key is empty' % argname)
883
884 if value.strip() != value:
885 parser.error('%s value has whitespace' % argname)
886 if not value:
887 parser.error('%s value is empty' % argname)
888
889 if expiration is not None:
890 try:
891 expiration = int(expiration)
892 except ValueError:
893 parser.error('%s expiration is not an integer' % argname)
894 if expiration <= 0:
895 parser.error('%s expiration should be positive' % argname)
896 if expiration % 60 != 0:
897 parser.error('%s expiration is not divisible by 60' % argname)
898
899
maruelaf6b06c2017-06-08 06:26:53 -0700900def process_filter_options(parser, options):
901 for key, value in options.dimensions:
Brad Hallf78187a2018-10-19 17:08:55 +0000902 _validate_filter_option(parser, key, value, None, 'dimension')
903 for key, value, exp in options.optional_dimensions:
904 _validate_filter_option(parser, key, value, exp, 'optional-dimension')
maruelaf6b06c2017-06-08 06:26:53 -0700905 options.dimensions.sort()
906
907
Vadim Shtayurab450c602014-05-12 19:23:25 -0700908def add_sharding_options(parser):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400909 parser.sharding_group = optparse.OptionGroup(parser, 'Sharding options')
Vadim Shtayurab450c602014-05-12 19:23:25 -0700910 parser.sharding_group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700911 '--shards', type='int', default=1, metavar='NUMBER',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700912 help='Number of shards to trigger and collect.')
913 parser.add_option_group(parser.sharding_group)
914
915
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400916def add_trigger_options(parser):
917 """Adds all options to trigger a task on Swarming."""
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -0500918 isolateserver.add_isolate_server_options(parser)
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -0400919 add_filter_options(parser)
920
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -0400921 group = optparse.OptionGroup(parser, 'TaskSlice properties')
maruel681d6802017-01-17 16:56:03 -0800922 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700923 '-s', '--isolated', metavar='HASH',
Marc-Antoine Ruel185ded42015-01-28 20:49:18 -0500924 help='Hash of the .isolated to grab from the isolate server')
maruel681d6802017-01-17 16:56:03 -0800925 group.add_option(
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -0500926 '-e', '--env', default=[], action='append', nargs=2, metavar='FOO bar',
Vadim Shtayurab450c602014-05-12 19:23:25 -0700927 help='Environment variables to set')
maruel681d6802017-01-17 16:56:03 -0800928 group.add_option(
Robert Iannuccibf5f84c2017-11-22 12:56:50 -0800929 '--env-prefix', default=[], action='append', nargs=2,
930 metavar='VAR local/path',
931 help='Prepend task-relative `local/path` to the task\'s VAR environment '
932 'variable using os-appropriate pathsep character. Can be specified '
933 'multiple times for the same VAR to add multiple paths.')
934 group.add_option(
Marc-Antoine Ruel02196392014-10-17 16:29:43 -0400935 '--idempotent', action='store_true', default=False,
936 help='When set, the server will actively try to find a previous task '
937 'with the same parameter and return this result instead if possible')
maruel681d6802017-01-17 16:56:03 -0800938 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700939 '--secret-bytes-path', metavar='FILE',
iannuccidc80dfb2016-10-28 12:50:20 -0700940 help='The optional path to a file containing the secret_bytes to use with'
941 'this task.')
maruel681d6802017-01-17 16:56:03 -0800942 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700943 '--hard-timeout', type='int', default=60*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400944 help='Seconds to allow the task to complete.')
maruel681d6802017-01-17 16:56:03 -0800945 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700946 '--io-timeout', type='int', default=20*60, metavar='SECS',
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -0400947 help='Seconds to allow the task to be silent.')
maruel681d6802017-01-17 16:56:03 -0800948 group.add_option(
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -0500949 '--raw-cmd', action='store_true', default=False,
950 help='When set, the command after -- is used as-is without run_isolated. '
maruel0a25f6c2017-05-10 10:43:23 -0700951 'In this case, the .isolated file is expected to not have a command')
maruel681d6802017-01-17 16:56:03 -0800952 group.add_option(
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -0500953 '--relative-cwd',
954 help='Ignore the isolated \'relative_cwd\' and use this one instead; '
955 'requires --raw-cmd')
956 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700957 '--cipd-package', action='append', default=[], metavar='PKG',
958 help='CIPD packages to install on the Swarming bot. Uses the format: '
borenet02f772b2016-06-22 12:42:19 -0700959 'path:package_name:version')
maruel681d6802017-01-17 16:56:03 -0800960 group.add_option(
961 '--named-cache', action='append', nargs=2, default=[],
maruel5475ba62017-05-31 15:35:47 -0700962 metavar='NAME RELPATH',
maruel681d6802017-01-17 16:56:03 -0800963 help='"<name> <relpath>" items to keep a persistent bot managed cache')
964 group.add_option(
vadimsh93d167c2016-09-13 11:31:51 -0700965 '--service-account',
Vadim Shtayura2d83a942017-08-14 17:41:24 -0700966 help='Email of a service account to run the task as, or literal "bot" '
967 'string to indicate that the task should use the same account the '
968 'bot itself is using to authenticate to Swarming. Don\'t use task '
969 'service accounts if not given (default).')
maruel681d6802017-01-17 16:56:03 -0800970 group.add_option(
Robert Iannuccifafa7352018-06-13 17:08:17 +0000971 '--pool-task-template',
972 choices=('AUTO', 'CANARY_PREFER', 'CANARY_NEVER', 'SKIP'),
973 default='AUTO',
974 help='Set how you want swarming to apply the pool\'s TaskTemplate. '
975 'By default, the pool\'s TaskTemplate is automatically selected, '
976 'according the pool configuration on the server. Choices are: '
977 'AUTO, CANARY_PREFER, CANARY_NEVER, and SKIP (default: AUTO).')
978 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700979 '-o', '--output', action='append', default=[], metavar='PATH',
980 help='A list of files to return in addition to those written to '
981 '${ISOLATED_OUTDIR}. An error will occur if a file specified by'
982 'this option is also written directly to ${ISOLATED_OUTDIR}.')
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -0400983 group.add_option(
984 '--wait-for-capacity', action='store_true', default=False,
985 help='Instructs to leave the task PENDING even if there\'s no known bot '
986 'that could run this task, otherwise the task will be denied with '
987 'NO_RESOURCE')
maruel681d6802017-01-17 16:56:03 -0800988 parser.add_option_group(group)
989
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -0400990 group = optparse.OptionGroup(parser, 'TaskRequest details')
maruel681d6802017-01-17 16:56:03 -0800991 group.add_option(
Marc-Antoine Ruel486c9b52018-07-23 19:30:47 +0000992 '--priority', type='int', default=200,
maruel681d6802017-01-17 16:56:03 -0800993 help='The lower value, the more important the task is')
994 group.add_option(
maruel5475ba62017-05-31 15:35:47 -0700995 '-T', '--task-name', metavar='NAME',
maruel681d6802017-01-17 16:56:03 -0800996 help='Display name of the task. Defaults to '
997 '<base_name>/<dimensions>/<isolated hash>/<timestamp> if an '
998 'isolated file is provided, if a hash is provided, it defaults to '
999 '<user>/<dimensions>/<isolated hash>/<timestamp>')
1000 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001001 '--tags', action='append', default=[], metavar='FOO:BAR',
maruel681d6802017-01-17 16:56:03 -08001002 help='Tags to assign to the task.')
1003 group.add_option(
1004 '--user', default='',
1005 help='User associated with the task. Defaults to authenticated user on '
1006 'the server.')
1007 group.add_option(
maruel5475ba62017-05-31 15:35:47 -07001008 '--expiration', type='int', default=6*60*60, metavar='SECS',
maruel681d6802017-01-17 16:56:03 -08001009 help='Seconds to allow the task to be pending for a bot to run before '
1010 'this task request expires.')
1011 group.add_option(
1012 '--deadline', type='int', dest='expiration',
1013 help=optparse.SUPPRESS_HELP)
1014 parser.add_option_group(group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001015
1016
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001017def process_trigger_options(parser, options, args):
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001018 """Processes trigger options and does preparatory steps.
1019
1020 Returns:
1021 NewTaskRequest instance.
1022 """
maruelaf6b06c2017-06-08 06:26:53 -07001023 process_filter_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001024 options.env = dict(options.env)
maruel0a25f6c2017-05-10 10:43:23 -07001025 if args and args[0] == '--':
1026 args = args[1:]
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001027
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001028 if not options.dimensions:
1029 parser.error('Please at least specify one --dimension')
Marc-Antoine Ruel33d198c2018-11-27 21:12:16 +00001030 if not any(k == 'pool' for k, _v in options.dimensions):
1031 parser.error('You must specify --dimension pool <value>')
maruel0a25f6c2017-05-10 10:43:23 -07001032 if not all(len(t.split(':', 1)) == 2 for t in options.tags):
1033 parser.error('--tags must be in the format key:value')
1034 if options.raw_cmd and not args:
1035 parser.error(
1036 'Arguments with --raw-cmd should be passed after -- as command '
1037 'delimiter.')
1038 if options.isolate_server and not options.namespace:
1039 parser.error(
1040 '--namespace must be a valid value when --isolate-server is used')
1041 if not options.isolated and not options.raw_cmd:
1042 parser.error('Specify at least one of --raw-cmd or --isolated or both')
1043
1044 # Isolated
1045 # --isolated is required only if --raw-cmd wasn't provided.
1046 # TODO(maruel): --isolate-server may be optional as Swarming may have its own
1047 # preferred server.
1048 isolateserver.process_isolate_server_options(
1049 parser, options, False, not options.raw_cmd)
1050 inputs_ref = None
1051 if options.isolate_server:
1052 inputs_ref = FilesRef(
1053 isolated=options.isolated,
1054 isolatedserver=options.isolate_server,
1055 namespace=options.namespace)
1056
1057 # Command
1058 command = None
1059 extra_args = None
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001060 if options.raw_cmd:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001061 command = args
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001062 if options.relative_cwd:
1063 a = os.path.normpath(os.path.abspath(options.relative_cwd))
1064 if not a.startswith(os.getcwd()):
1065 parser.error(
1066 '--relative-cwd must not try to escape the working directory')
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001067 else:
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001068 if options.relative_cwd:
1069 parser.error('--relative-cwd requires --raw-cmd')
maruel0a25f6c2017-05-10 10:43:23 -07001070 extra_args = args
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001071
maruel0a25f6c2017-05-10 10:43:23 -07001072 # CIPD
borenet02f772b2016-06-22 12:42:19 -07001073 cipd_packages = []
1074 for p in options.cipd_package:
1075 split = p.split(':', 2)
1076 if len(split) != 3:
1077 parser.error('CIPD packages must take the form: path:package:version')
1078 cipd_packages.append(CipdPackage(
1079 package_name=split[1],
1080 path=split[0],
1081 version=split[2]))
1082 cipd_input = None
1083 if cipd_packages:
1084 cipd_input = CipdInput(
1085 client_package=None,
1086 packages=cipd_packages,
1087 server=None)
1088
maruel0a25f6c2017-05-10 10:43:23 -07001089 # Secrets
iannuccidc80dfb2016-10-28 12:50:20 -07001090 secret_bytes = None
1091 if options.secret_bytes_path:
Marc-Antoine Ruel5c98fa72018-05-18 12:19:59 -04001092 with open(options.secret_bytes_path, 'rb') as f:
iannuccidc80dfb2016-10-28 12:50:20 -07001093 secret_bytes = f.read().encode('base64')
1094
maruel0a25f6c2017-05-10 10:43:23 -07001095 # Named caches
maruel681d6802017-01-17 16:56:03 -08001096 caches = [
1097 {u'name': unicode(i[0]), u'path': unicode(i[1])}
1098 for i in options.named_cache
1099 ]
maruel0a25f6c2017-05-10 10:43:23 -07001100
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001101 env_prefixes = {}
1102 for k, v in options.env_prefix:
1103 env_prefixes.setdefault(k, []).append(v)
1104
Brad Hallf78187a2018-10-19 17:08:55 +00001105 # Get dimensions into the key/value format we can manipulate later.
1106 orig_dims = [
1107 {'key': key, 'value': value} for key, value in options.dimensions]
1108 orig_dims.sort(key=lambda x: (x['key'], x['value']))
1109
1110 # Construct base properties that we will use for all the slices, adding in
1111 # optional dimensions for the fallback slices.
maruel77f720b2015-09-15 12:35:22 -07001112 properties = TaskProperties(
maruel681d6802017-01-17 16:56:03 -08001113 caches=caches,
borenet02f772b2016-06-22 12:42:19 -07001114 cipd_input=cipd_input,
maruel0a25f6c2017-05-10 10:43:23 -07001115 command=command,
Marc-Antoine Ruelba1bf222017-12-21 21:41:01 -05001116 relative_cwd=options.relative_cwd,
Brad Hallf78187a2018-10-19 17:08:55 +00001117 dimensions=orig_dims,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001118 env=options.env,
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001119 env_prefixes=[StringListPair(k, v) for k, v in env_prefixes.iteritems()],
maruel77f720b2015-09-15 12:35:22 -07001120 execution_timeout_secs=options.hard_timeout,
maruel0a25f6c2017-05-10 10:43:23 -07001121 extra_args=extra_args,
maruel77f720b2015-09-15 12:35:22 -07001122 grace_period_secs=30,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001123 idempotent=options.idempotent,
maruel77f720b2015-09-15 12:35:22 -07001124 inputs_ref=inputs_ref,
aludwincc5524e2016-10-28 10:25:24 -07001125 io_timeout_secs=options.io_timeout,
iannuccidc80dfb2016-10-28 12:50:20 -07001126 outputs=options.output,
1127 secret_bytes=secret_bytes)
Brad Hallf78187a2018-10-19 17:08:55 +00001128
1129 slices = []
1130
1131 # Group the optional dimensions by expiration.
1132 dims_by_exp = {}
1133 for key, value, exp_secs in options.optional_dimensions:
1134 dims_by_exp.setdefault(int(exp_secs), []).append(
1135 {'key': key, 'value': value})
1136
1137 # Create the optional slices with expiration deltas, we fix up the properties
1138 # below.
1139 last_exp = 0
1140 for expiration_secs in sorted(dims_by_exp):
1141 t = TaskSlice(
1142 expiration_secs=expiration_secs - last_exp,
1143 properties=properties,
1144 wait_for_capacity=False)
1145 slices.append(t)
1146 last_exp = expiration_secs
1147
1148 # Add back in the default slice (the last one).
1149 exp = max(int(options.expiration) - last_exp, 60)
1150 base_task_slice = TaskSlice(
1151 expiration_secs=exp,
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001152 properties=properties,
1153 wait_for_capacity=options.wait_for_capacity)
Brad Hallf78187a2018-10-19 17:08:55 +00001154 slices.append(base_task_slice)
1155
Brad Hall7f463e62018-11-16 16:13:30 +00001156 # Add optional dimensions to the task slices, replacing a dimension that
1157 # has the same key if it is a dimension where repeating isn't valid (otherwise
1158 # we append it). Currently the only dimension we can repeat is "caches"; the
1159 # rest (os, cpu, etc) shouldn't be repeated.
Brad Hallf78187a2018-10-19 17:08:55 +00001160 extra_dims = []
Brad Hall7f463e62018-11-16 16:13:30 +00001161 for i, (_, kvs) in enumerate(sorted(dims_by_exp.iteritems(), reverse=True)):
Brad Hallf78187a2018-10-19 17:08:55 +00001162 dims = list(orig_dims)
Brad Hall7f463e62018-11-16 16:13:30 +00001163 # Replace or append the key/value pairs for this expiration in extra_dims;
1164 # we keep extra_dims around because we are iterating backwards and filling
1165 # in slices with shorter expirations. Dimensions expire as time goes on so
1166 # the slices that expire earlier will generally have more dimensions.
1167 for kv in kvs:
1168 if kv['key'] == 'caches':
1169 extra_dims.append(kv)
1170 else:
1171 extra_dims = [x for x in extra_dims if x['key'] != kv['key']] + [kv]
1172 # Then, add all the optional dimensions to the original dimension set, again
1173 # replacing if needed.
1174 for kv in extra_dims:
1175 if kv['key'] == 'caches':
1176 dims.append(kv)
1177 else:
1178 dims = [x for x in dims if x['key'] != kv['key']] + [kv]
Brad Hallf78187a2018-10-19 17:08:55 +00001179 dims.sort(key=lambda x: (x['key'], x['value']))
1180 slice_properties = properties._replace(dimensions=dims)
1181 slices[-2 - i] = slices[-2 - i]._replace(properties=slice_properties)
1182
maruel77f720b2015-09-15 12:35:22 -07001183 return NewTaskRequest(
maruel0a25f6c2017-05-10 10:43:23 -07001184 name=default_task_name(options),
maruel77f720b2015-09-15 12:35:22 -07001185 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''),
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001186 priority=options.priority,
Brad Hallf78187a2018-10-19 17:08:55 +00001187 task_slices=slices,
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001188 service_account=options.service_account,
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001189 tags=options.tags,
Robert Iannuccifafa7352018-06-13 17:08:17 +00001190 user=options.user,
1191 pool_task_template=options.pool_task_template)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001192
1193
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001194class TaskOutputStdoutOption(optparse.Option):
1195 """Where to output the each task's console output (stderr/stdout).
1196
1197 The output will be;
1198 none - not be downloaded.
1199 json - stored in summary.json file *only*.
1200 console - shown on stdout *only*.
1201 all - stored in summary.json and shown on stdout.
1202 """
1203
1204 choices = ['all', 'json', 'console', 'none']
1205
1206 def __init__(self, *args, **kw):
1207 optparse.Option.__init__(
1208 self,
1209 *args,
1210 choices=self.choices,
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001211 default=['console', 'json'],
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001212 help=re.sub('\s\s*', ' ', self.__doc__),
1213 **kw)
1214
1215 def convert_value(self, opt, value):
1216 if value not in self.choices:
1217 raise optparse.OptionValueError("%s must be one of %s not %r" % (
1218 self.get_opt_string(), self.choices, value))
1219 stdout_to = []
1220 if value == 'all':
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001221 stdout_to = ['console', 'json']
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001222 elif value != 'none':
1223 stdout_to = [value]
1224 return stdout_to
1225
1226
maruel@chromium.org0437a732013-08-27 16:05:52 +00001227def add_collect_options(parser):
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001228 parser.server_group.add_option(
Marc-Antoine Ruele831f052018-04-20 15:01:03 -04001229 '-t', '--timeout', type='float', default=0.,
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001230 help='Timeout to wait for result, set to -1 for no timeout and get '
1231 'current state; defaults to waiting until the task completes')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001232 parser.group_logging.add_option(
1233 '--decorate', action='store_true', help='Decorate output')
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001234 parser.group_logging.add_option(
1235 '--print-status-updates', action='store_true',
1236 help='Print periodic status updates')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001237 parser.task_output_group = optparse.OptionGroup(parser, 'Task output')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001238 parser.task_output_group.add_option(
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001239 '--task-summary-json',
1240 metavar='FILE',
1241 help='Dump a summary of task results to this file as json. It contains '
1242 'only shards statuses as know to server directly. Any output files '
1243 'emitted by the task can be collected by using --task-output-dir')
1244 parser.task_output_group.add_option(
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001245 '--task-output-dir',
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001246 metavar='DIR',
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001247 help='Directory to put task results into. When the task finishes, this '
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001248 'directory contains per-shard directory with output files produced '
1249 'by shards: <task-output-dir>/<zero-based-shard-index>/.')
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001250 parser.task_output_group.add_option(TaskOutputStdoutOption(
Marc-Antoine Ruel28488842017-09-12 18:09:17 -04001251 '--task-output-stdout'))
maruel9531ce02016-04-13 06:11:23 -07001252 parser.task_output_group.add_option(
Takuto Ikuta1e6072c2018-11-06 20:42:43 +00001253 '--filepath-filter',
1254 help='This is regexp filter used to specify downloaded filepath when '
1255 'collecting isolated output.')
1256 parser.task_output_group.add_option(
maruel9531ce02016-04-13 06:11:23 -07001257 '--perf', action='store_true', default=False,
1258 help='Includes performance statistics')
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001259 parser.add_option_group(parser.task_output_group)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001260
1261
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001262def process_collect_options(parser, options):
1263 # Only negative -1 is allowed, disallow other negative values.
1264 if options.timeout != -1 and options.timeout < 0:
1265 parser.error('Invalid --timeout value')
1266
1267
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001268@subcommand.usage('bots...')
1269def CMDbot_delete(parser, args):
1270 """Forcibly deletes bots from the Swarming server."""
1271 parser.add_option(
1272 '-f', '--force', action='store_true',
1273 help='Do not prompt for confirmation')
1274 options, args = parser.parse_args(args)
1275 if not args:
maruelfd0a90c2016-06-10 11:51:10 -07001276 parser.error('Please specify bots to delete')
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001277
1278 bots = sorted(args)
1279 if not options.force:
1280 print('Delete the following bots?')
1281 for bot in bots:
1282 print(' %s' % bot)
1283 if raw_input('Continue? [y/N] ') not in ('y', 'Y'):
1284 print('Goodbye.')
1285 return 1
1286
1287 result = 0
1288 for bot in bots:
smut281c3902018-05-30 17:50:05 -07001289 url = '%s/_ah/api/swarming/v1/bot/%s/delete' % (options.swarming, bot)
vadimshe4c0e242015-09-30 11:53:54 -07001290 if net.url_read_json(url, data={}, method='POST') is None:
1291 print('Deleting %s failed. Probably already gone' % bot)
Marc-Antoine Ruel13e7c882015-03-26 18:19:10 -04001292 result = 1
1293 return result
1294
1295
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001296def CMDbots(parser, args):
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001297 """Returns information about the bots connected to the Swarming server."""
1298 add_filter_options(parser)
1299 parser.filter_group.add_option(
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001300 '--dead-only', action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001301 help='Filter out bots alive, useful to reap them and reimage broken bots')
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001302 parser.filter_group.add_option(
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001303 '-k', '--keep-dead', action='store_true',
maruelaf6b06c2017-06-08 06:26:53 -07001304 help='Keep both dead and alive bots')
1305 parser.filter_group.add_option(
1306 '--busy', action='store_true', help='Keep only busy bots')
1307 parser.filter_group.add_option(
1308 '--idle', action='store_true', help='Keep only idle bots')
1309 parser.filter_group.add_option(
1310 '--mp', action='store_true',
1311 help='Keep only Machine Provider managed bots')
1312 parser.filter_group.add_option(
1313 '--non-mp', action='store_true',
1314 help='Keep only non Machine Provider managed bots')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001315 parser.filter_group.add_option(
1316 '-b', '--bare', action='store_true',
Marc-Antoine Ruele7b00162014-03-12 16:59:01 -04001317 help='Do not print out dimensions')
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001318 options, args = parser.parse_args(args)
maruelaf6b06c2017-06-08 06:26:53 -07001319 process_filter_options(parser, options)
Marc-Antoine Ruel28083112014-03-13 16:34:04 -04001320
1321 if options.keep_dead and options.dead_only:
maruelaf6b06c2017-06-08 06:26:53 -07001322 parser.error('Use only one of --keep-dead or --dead-only')
1323 if options.busy and options.idle:
1324 parser.error('Use only one of --busy or --idle')
1325 if options.mp and options.non_mp:
1326 parser.error('Use only one of --mp or --non-mp')
Vadim Shtayura6b555c12014-07-23 16:22:18 -07001327
smut281c3902018-05-30 17:50:05 -07001328 url = options.swarming + '/_ah/api/swarming/v1/bots/list?'
maruelaf6b06c2017-06-08 06:26:53 -07001329 values = []
1330 if options.dead_only:
1331 values.append(('is_dead', 'TRUE'))
1332 elif options.keep_dead:
1333 values.append(('is_dead', 'NONE'))
1334 else:
1335 values.append(('is_dead', 'FALSE'))
Marc-Antoine Ruelc6c579e2014-09-08 18:43:45 -04001336
maruelaf6b06c2017-06-08 06:26:53 -07001337 if options.busy:
1338 values.append(('is_busy', 'TRUE'))
1339 elif options.idle:
1340 values.append(('is_busy', 'FALSE'))
1341 else:
1342 values.append(('is_busy', 'NONE'))
1343
1344 if options.mp:
1345 values.append(('is_mp', 'TRUE'))
1346 elif options.non_mp:
1347 values.append(('is_mp', 'FALSE'))
1348 else:
1349 values.append(('is_mp', 'NONE'))
1350
1351 for key, value in options.dimensions:
1352 values.append(('dimensions', '%s:%s' % (key, value)))
1353 url += urllib.urlencode(values)
1354 try:
1355 data, yielder = get_yielder(url, 0)
1356 bots = data.get('items') or []
1357 for items in yielder():
1358 if items:
1359 bots.extend(items)
1360 except Failure as e:
1361 sys.stderr.write('\n%s\n' % e)
1362 return 1
maruel77f720b2015-09-15 12:35:22 -07001363 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']):
maruelaf6b06c2017-06-08 06:26:53 -07001364 print bot['bot_id']
1365 if not options.bare:
1366 dimensions = {i['key']: i.get('value') for i in bot.get('dimensions', {})}
1367 print ' %s' % json.dumps(dimensions, sort_keys=True)
1368 if bot.get('task_id'):
1369 print ' task: %s' % bot['task_id']
Marc-Antoine Ruel819fb162014-03-12 16:38:26 -04001370 return 0
1371
1372
maruelfd0a90c2016-06-10 11:51:10 -07001373@subcommand.usage('task_id')
1374def CMDcancel(parser, args):
1375 """Cancels a task."""
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001376 parser.add_option(
1377 '-k', '--kill-running', action='store_true', default=False,
1378 help='Kill the task even if it was running')
maruelfd0a90c2016-06-10 11:51:10 -07001379 options, args = parser.parse_args(args)
1380 if not args:
1381 parser.error('Please specify the task to cancel')
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001382 data = {'kill_running': options.kill_running}
maruelfd0a90c2016-06-10 11:51:10 -07001383 for task_id in args:
smut281c3902018-05-30 17:50:05 -07001384 url = '%s/_ah/api/swarming/v1/task/%s/cancel' % (options.swarming, task_id)
Marc-Antoine Ruel2e52c552018-03-26 19:27:36 -04001385 resp = net.url_read_json(url, data=data, method='POST')
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001386 if resp is None:
maruelfd0a90c2016-06-10 11:51:10 -07001387 print('Deleting %s failed. Probably already gone' % task_id)
1388 return 1
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001389 logging.info('%s', resp)
maruelfd0a90c2016-06-10 11:51:10 -07001390 return 0
1391
1392
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001393@subcommand.usage('--json file | task_id...')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001394def CMDcollect(parser, args):
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001395 """Retrieves results of one or multiple Swarming task by its ID.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001396
1397 The result can be in multiple part if the execution was sharded. It can
1398 potentially have retries.
1399 """
1400 add_collect_options(parser)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001401 parser.add_option(
1402 '-j', '--json',
1403 help='Load the task ids from .json as saved by trigger --dump-json')
maruel77f720b2015-09-15 12:35:22 -07001404 options, args = parser.parse_args(args)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001405 process_collect_options(parser, options)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001406 if not args and not options.json:
1407 parser.error('Must specify at least one task id or --json.')
1408 if args and options.json:
1409 parser.error('Only use one of task id or --json.')
1410
1411 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001412 options.json = unicode(os.path.abspath(options.json))
Marc-Antoine Ruel9025a782015-03-17 16:42:59 -04001413 try:
maruel1ceb3872015-10-14 06:10:44 -07001414 with fs.open(options.json, 'rb') as f:
maruel71c61c82016-02-22 06:52:05 -08001415 data = json.load(f)
1416 except (IOError, ValueError):
1417 parser.error('Failed to open %s' % options.json)
1418 try:
1419 tasks = sorted(
1420 data['tasks'].itervalues(), key=lambda x: x['shard_index'])
1421 args = [t['task_id'] for t in tasks]
1422 except (KeyError, TypeError):
1423 parser.error('Failed to process %s' % options.json)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001424 if not options.timeout:
Marc-Antoine Ruelb73066b2018-04-19 20:16:55 -04001425 # Take in account all the task slices.
1426 offset = 0
1427 for s in data['request']['task_slices']:
1428 m = (offset + s['properties']['execution_timeout_secs'] +
1429 s['expiration_secs'])
1430 if m > options.timeout:
1431 options.timeout = m
1432 offset += s['expiration_secs']
Marc-Antoine Ruel9fc42612018-04-20 08:34:22 -04001433 options.timeout += 10.
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001434 else:
1435 valid = frozenset('0123456789abcdef')
1436 if any(not valid.issuperset(task_id) for task_id in args):
1437 parser.error('Task ids are 0-9a-f.')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001438
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001439 try:
1440 return collect(
1441 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001442 args,
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001443 options.timeout,
1444 options.decorate,
1445 options.print_status_updates,
1446 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001447 options.task_output_dir,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001448 options.task_output_stdout,
Takuto Ikuta1e6072c2018-11-06 20:42:43 +00001449 options.perf,
1450 options.filepath_filter)
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001451 except Failure:
1452 on_error.report(None)
1453 return 1
1454
1455
maruel77f720b2015-09-15 12:35:22 -07001456@subcommand.usage('[method name]')
Marc-Antoine Ruel833f5eb2018-04-25 16:49:40 -04001457def CMDpost(parser, args):
1458 """Sends a JSON RPC POST to one API endpoint and prints out the raw result.
1459
1460 Input data must be sent to stdin, result is printed to stdout.
1461
1462 If HTTP response code >= 400, returns non-zero.
1463 """
1464 options, args = parser.parse_args(args)
1465 if len(args) != 1:
1466 parser.error('Must specify only API name')
smut281c3902018-05-30 17:50:05 -07001467 url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
Marc-Antoine Ruel833f5eb2018-04-25 16:49:40 -04001468 data = sys.stdin.read()
1469 try:
1470 resp = net.url_read(url, data=data, method='POST')
1471 except net.TimeoutError:
1472 sys.stderr.write('Timeout!\n')
1473 return 1
1474 if not resp:
1475 sys.stderr.write('No response!\n')
1476 return 1
1477 sys.stdout.write(resp)
1478 return 0
1479
1480
1481@subcommand.usage('[method name]')
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001482def CMDquery(parser, args):
maruel77f720b2015-09-15 12:35:22 -07001483 """Returns raw JSON information via an URL endpoint. Use 'query-list' to
1484 gather the list of API methods from the server.
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001485
1486 Examples:
maruelaf6b06c2017-06-08 06:26:53 -07001487 Raw task request and results:
1488 swarming.py query -S server-url.com task/123456/request
1489 swarming.py query -S server-url.com task/123456/result
1490
maruel77f720b2015-09-15 12:35:22 -07001491 Listing all bots:
maruel84e77aa2015-10-21 06:37:24 -07001492 swarming.py query -S server-url.com bots/list
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001493
maruelaf6b06c2017-06-08 06:26:53 -07001494 Listing last 10 tasks on a specific bot named 'bot1':
1495 swarming.py query -S server-url.com --limit 10 bot/bot1/tasks
maruel84e77aa2015-10-21 06:37:24 -07001496
maruelaf6b06c2017-06-08 06:26:53 -07001497 Listing last 10 tasks with tags os:Ubuntu-14.04 and pool:Chrome. Note that
maruel84e77aa2015-10-21 06:37:24 -07001498 quoting is important!:
1499 swarming.py query -S server-url.com --limit 10 \\
maruelaf6b06c2017-06-08 06:26:53 -07001500 'tasks/list?tags=os:Ubuntu-14.04&tags=pool:Chrome'
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001501 """
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001502 parser.add_option(
1503 '-L', '--limit', type='int', default=200,
1504 help='Limit to enforce on limitless items (like number of tasks); '
1505 'default=%default')
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001506 parser.add_option(
1507 '--json', help='Path to JSON output file (otherwise prints to stdout)')
maruel77f720b2015-09-15 12:35:22 -07001508 parser.add_option(
1509 '--progress', action='store_true',
1510 help='Prints a dot at each request to show progress')
1511 options, args = parser.parse_args(args)
marueld8aba222015-09-03 12:21:19 -07001512 if len(args) != 1:
maruel77f720b2015-09-15 12:35:22 -07001513 parser.error(
1514 'Must specify only method name and optionally query args properly '
1515 'escaped.')
smut281c3902018-05-30 17:50:05 -07001516 base_url = options.swarming + '/_ah/api/swarming/v1/' + args[0]
maruelaf6b06c2017-06-08 06:26:53 -07001517 try:
1518 data, yielder = get_yielder(base_url, options.limit)
1519 for items in yielder():
1520 if items:
1521 data['items'].extend(items)
maruel77f720b2015-09-15 12:35:22 -07001522 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001523 sys.stderr.write('.')
1524 sys.stderr.flush()
1525 except Failure as e:
1526 sys.stderr.write('\n%s\n' % e)
1527 return 1
maruel77f720b2015-09-15 12:35:22 -07001528 if options.progress:
maruelaf6b06c2017-06-08 06:26:53 -07001529 sys.stderr.write('\n')
1530 sys.stderr.flush()
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001531 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001532 options.json = unicode(os.path.abspath(options.json))
1533 tools.write_json(options.json, data, True)
Paweł Hajdan, Jr53ef0132015-03-20 17:49:18 +01001534 else:
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001535 try:
maruel77f720b2015-09-15 12:35:22 -07001536 tools.write_json(sys.stdout, data, False)
Marc-Antoine Ruelcda90ee2015-03-23 15:13:20 -04001537 sys.stdout.write('\n')
1538 except IOError:
1539 pass
Marc-Antoine Ruel79940ae2014-09-23 17:55:41 -04001540 return 0
1541
1542
maruel77f720b2015-09-15 12:35:22 -07001543def CMDquery_list(parser, args):
1544 """Returns list of all the Swarming APIs that can be used with command
1545 'query'.
1546 """
1547 parser.add_option(
1548 '--json', help='Path to JSON output file (otherwise prints to stdout)')
1549 options, args = parser.parse_args(args)
1550 if args:
1551 parser.error('No argument allowed.')
1552
1553 try:
1554 apis = endpoints_api_discovery_apis(options.swarming)
1555 except APIError as e:
1556 parser.error(str(e))
1557 if options.json:
maruel1ceb3872015-10-14 06:10:44 -07001558 options.json = unicode(os.path.abspath(options.json))
1559 with fs.open(options.json, 'wb') as f:
maruel77f720b2015-09-15 12:35:22 -07001560 json.dump(apis, f)
1561 else:
1562 help_url = (
1563 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' %
1564 options.swarming)
maruel11e31af2017-02-15 07:30:50 -08001565 for i, (api_id, api) in enumerate(sorted(apis.iteritems())):
1566 if i:
1567 print('')
maruel77f720b2015-09-15 12:35:22 -07001568 print api_id
maruel11e31af2017-02-15 07:30:50 -08001569 print ' ' + api['description'].strip()
1570 if 'resources' in api:
1571 # Old.
1572 for j, (resource_name, resource) in enumerate(
1573 sorted(api['resources'].iteritems())):
1574 if j:
1575 print('')
1576 for method_name, method in sorted(resource['methods'].iteritems()):
1577 # Only list the GET ones.
1578 if method['httpMethod'] != 'GET':
1579 continue
1580 print '- %s.%s: %s' % (
1581 resource_name, method_name, method['path'])
1582 print('\n'.join(
Sergey Berezina269e1a2018-05-16 16:55:12 -07001583 ' ' + l for l in textwrap.wrap(
1584 method.get('description', 'No description'), 78)))
maruel11e31af2017-02-15 07:30:50 -08001585 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1586 else:
1587 # New.
1588 for method_name, method in sorted(api['methods'].iteritems()):
maruel77f720b2015-09-15 12:35:22 -07001589 # Only list the GET ones.
1590 if method['httpMethod'] != 'GET':
1591 continue
maruel11e31af2017-02-15 07:30:50 -08001592 print '- %s: %s' % (method['id'], method['path'])
1593 print('\n'.join(
1594 ' ' + l for l in textwrap.wrap(method['description'], 78)))
maruel77f720b2015-09-15 12:35:22 -07001595 print ' %s%s%s' % (help_url, api['servicePath'], method['id'])
1596 return 0
1597
1598
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001599@subcommand.usage('(hash|isolated) [-- extra_args]')
maruel@chromium.org0437a732013-08-27 16:05:52 +00001600def CMDrun(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001601 """Triggers a task and wait for the results.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001602
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001603 Basically, does everything to run a command remotely.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001604 """
1605 add_trigger_options(parser)
1606 add_collect_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001607 add_sharding_options(parser)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001608 options, args = parser.parse_args(args)
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001609 process_collect_options(parser, options)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001610 task_request = process_trigger_options(parser, options, args)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001611 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001612 tasks = trigger_task_shards(
1613 options.swarming, task_request, options.shards)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001614 except Failure as e:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001615 on_error.report(
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001616 'Failed to trigger %s(%s): %s' %
maruel0a25f6c2017-05-10 10:43:23 -07001617 (task_request.name, args[0], e.args[0]))
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001618 return 1
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001619 if not tasks:
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001620 on_error.report('Failed to trigger the task.')
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001621 return 1
maruel0a25f6c2017-05-10 10:43:23 -07001622 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001623 task_ids = [
1624 t['task_id']
1625 for t in sorted(tasks.itervalues(), key=lambda x: x['shard_index'])
1626 ]
Marc-Antoine Ruelf24f09c2018-03-23 16:06:18 -04001627 if not options.timeout:
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001628 offset = 0
1629 for s in task_request.task_slices:
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001630 m = (offset + s.properties.execution_timeout_secs +
1631 s.expiration_secs)
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001632 if m > options.timeout:
1633 options.timeout = m
Marc-Antoine Ruel1f835c72018-05-25 12:29:42 -04001634 offset += s.expiration_secs
Marc-Antoine Ruel3a030bc2018-04-23 10:31:25 -04001635 options.timeout += 10.
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001636 try:
1637 return collect(
1638 options.swarming,
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001639 task_ids,
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001640 options.timeout,
Vadim Shtayura86a2cef2014-04-18 11:13:39 -07001641 options.decorate,
Vadim Shtayurae3fbd102014-04-29 17:05:21 -07001642 options.print_status_updates,
Vadim Shtayurac8437bf2014-07-09 19:45:36 -07001643 options.task_summary_json,
maruel9531ce02016-04-13 06:11:23 -07001644 options.task_output_dir,
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001645 options.task_output_stdout,
Takuto Ikuta1e6072c2018-11-06 20:42:43 +00001646 options.perf,
1647 options.filepath_filter)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001648 except Failure:
1649 on_error.report(None)
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001650 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001651
1652
maruel18122c62015-10-23 06:31:23 -07001653@subcommand.usage('task_id -- <extra_args>')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001654def CMDreproduce(parser, args):
1655 """Runs a task locally that was triggered on the server.
1656
1657 This running locally the same commands that have been run on the bot. The data
1658 downloaded will be in a subdirectory named 'work' of the current working
1659 directory.
maruel18122c62015-10-23 06:31:23 -07001660
1661 You can pass further additional arguments to the target command by passing
1662 them after --.
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001663 """
maruelc070e672016-02-22 17:32:57 -08001664 parser.add_option(
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001665 '--output', metavar='DIR', default='out',
maruelc070e672016-02-22 17:32:57 -08001666 help='Directory that will have results stored into')
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001667 parser.add_option(
1668 '--work', metavar='DIR', default='work',
1669 help='Directory to map the task input files into')
1670 parser.add_option(
1671 '--cache', metavar='DIR', default='cache',
1672 help='Directory that contains the input cache')
1673 parser.add_option(
1674 '--leak', action='store_true',
1675 help='Do not delete the working directory after execution')
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001676 options, args = parser.parse_args(args)
maruel18122c62015-10-23 06:31:23 -07001677 extra_args = []
1678 if not args:
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001679 parser.error('Must specify exactly one task id.')
maruel18122c62015-10-23 06:31:23 -07001680 if len(args) > 1:
1681 if args[1] == '--':
1682 if len(args) > 2:
1683 extra_args = args[2:]
1684 else:
1685 extra_args = args[1:]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001686
smut281c3902018-05-30 17:50:05 -07001687 url = options.swarming + '/_ah/api/swarming/v1/task/%s/request' % args[0]
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001688 request = net.url_read_json(url)
1689 if not request:
1690 print >> sys.stderr, 'Failed to retrieve request data for the task'
1691 return 1
1692
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001693 workdir = unicode(os.path.abspath(options.work))
maruele7cd38e2016-03-01 19:12:48 -08001694 if fs.isdir(workdir):
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001695 parser.error('Please delete the directory %r first' % options.work)
maruele7cd38e2016-03-01 19:12:48 -08001696 fs.mkdir(workdir)
iannucci31ab9192017-05-02 19:11:56 -07001697 cachedir = unicode(os.path.abspath('cipd_cache'))
1698 if not fs.exists(cachedir):
1699 fs.mkdir(cachedir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001700
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001701 properties = request['properties']
iannucci31ab9192017-05-02 19:11:56 -07001702 env = os.environ.copy()
1703 env['SWARMING_BOT_ID'] = 'reproduce'
1704 env['SWARMING_TASK_ID'] = 'reproduce'
maruel29ab2fd2015-10-16 11:44:01 -07001705 if properties.get('env'):
Marc-Antoine Ruel119b0842014-12-19 15:27:58 -05001706 logging.info('env: %r', properties['env'])
maruelb76604c2015-11-11 11:53:44 -08001707 for i in properties['env']:
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001708 key = i['key']
maruelb76604c2015-11-11 11:53:44 -08001709 if not i['value']:
1710 env.pop(key, None)
1711 else:
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001712 env[key] = i['value']
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001713
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001714 if properties.get('env_prefixes'):
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001715 env_prefixes = properties['env_prefixes']
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001716 logging.info('env_prefixes: %r', env_prefixes)
Marc-Antoine Ruel36e09792018-01-09 14:03:25 -05001717 for i in env_prefixes:
1718 key = i['key']
1719 paths = [os.path.normpath(os.path.join(workdir, p)) for p in i['value']]
Robert Iannuccibf5f84c2017-11-22 12:56:50 -08001720 cur = env.get(key)
1721 if cur:
1722 paths.append(cur)
1723 env[key] = os.path.pathsep.join(paths)
1724
iannucci31ab9192017-05-02 19:11:56 -07001725 command = []
nodir152cba62016-05-12 16:08:56 -07001726 if (properties.get('inputs_ref') or {}).get('isolated'):
maruel29ab2fd2015-10-16 11:44:01 -07001727 # Create the tree.
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +00001728 server_ref = isolate_storage.ServerRef(
maruel29ab2fd2015-10-16 11:44:01 -07001729 properties['inputs_ref']['isolatedserver'],
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +00001730 properties['inputs_ref']['namespace'])
1731 with isolateserver.get_storage(server_ref) as storage:
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001732 # Do not use MemoryContentAddressedCache here, as on 32-bits python,
1733 # inputs larger than ~1GiB will not fit in memory. This is effectively a
1734 # leak.
1735 policies = local_caching.CachePolicies(0, 0, 0, 0)
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001736 cache = local_caching.DiskContentAddressedCache(
Marc-Antoine Ruelb8513132018-11-20 19:48:53 +00001737 unicode(os.path.abspath(options.cache)), policies,
1738 server_ref.hash_algo, False)
maruel29ab2fd2015-10-16 11:44:01 -07001739 bundle = isolateserver.fetch_isolated(
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001740 properties['inputs_ref']['isolated'], storage, cache, workdir, False)
maruel29ab2fd2015-10-16 11:44:01 -07001741 command = bundle.command
1742 if bundle.relative_cwd:
1743 workdir = os.path.join(workdir, bundle.relative_cwd)
maruela1b9e552016-01-06 12:42:03 -08001744 command.extend(properties.get('extra_args') or [])
iannucci31ab9192017-05-02 19:11:56 -07001745
1746 if properties.get('command'):
1747 command.extend(properties['command'])
1748
Marc-Antoine Ruelc7243592018-05-24 17:04:04 -04001749 # https://chromium.googlesource.com/infra/luci/luci-py.git/+/master/appengine/swarming/doc/Magic-Values.md
Robert Iannucci24ae76a2018-02-26 12:51:18 -08001750 command = tools.fix_python_cmd(command, env)
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001751 if not options.output:
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001752 new_command = run_isolated.process_command(command, 'invalid', None)
1753 if new_command != command:
Marc-Antoine Ruel29ba75c2018-01-10 15:04:14 -05001754 parser.error('The task has outputs, you must use --output-dir')
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001755 else:
1756 # Make the path absolute, as the process will run from a subdirectory.
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001757 options.output = os.path.abspath(options.output)
Marc-Antoine Ruel88229872018-01-10 16:35:29 -05001758 new_command = run_isolated.process_command(
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001759 command, options.output, None)
1760 if not os.path.isdir(options.output):
1761 os.makedirs(options.output)
iannucci31ab9192017-05-02 19:11:56 -07001762 command = new_command
1763 file_path.ensure_command_has_abs_path(command, workdir)
1764
1765 if properties.get('cipd_input'):
1766 ci = properties['cipd_input']
1767 cp = ci['client_package']
1768 client_manager = cipd.get_client(
1769 ci['server'], cp['package_name'], cp['version'], cachedir)
1770
1771 with client_manager as client:
1772 by_path = collections.defaultdict(list)
1773 for pkg in ci['packages']:
1774 path = pkg['path']
1775 # cipd deals with 'root' as ''
1776 if path == '.':
1777 path = ''
1778 by_path[path].append((pkg['package_name'], pkg['version']))
1779 client.ensure(workdir, by_path, cache_dir=cachedir)
1780
maruel77f720b2015-09-15 12:35:22 -07001781 try:
Marc-Antoine Ruel95c21872018-01-10 14:24:28 -05001782 return subprocess42.call(command + extra_args, env=env, cwd=workdir)
maruel77f720b2015-09-15 12:35:22 -07001783 except OSError as e:
maruel29ab2fd2015-10-16 11:44:01 -07001784 print >> sys.stderr, 'Failed to run: %s' % ' '.join(command)
maruel77f720b2015-09-15 12:35:22 -07001785 print >> sys.stderr, str(e)
1786 return 1
Marc-Antoine Ruel5aeb3bb2018-06-16 13:11:02 +00001787 finally:
1788 # Do not delete options.cache.
1789 if not options.leak:
1790 file_path.rmtree(workdir)
Marc-Antoine Ruel13a81272014-10-07 20:16:43 -04001791
1792
maruel0eb1d1b2015-10-02 14:48:21 -07001793@subcommand.usage('bot_id')
1794def CMDterminate(parser, args):
1795 """Tells a bot to gracefully shut itself down as soon as it can.
1796
1797 This is done by completing whatever current task there is then exiting the bot
1798 process.
1799 """
1800 parser.add_option(
1801 '--wait', action='store_true', help='Wait for the bot to terminate')
1802 options, args = parser.parse_args(args)
1803 if len(args) != 1:
1804 parser.error('Please provide the bot id')
smut281c3902018-05-30 17:50:05 -07001805 url = options.swarming + '/_ah/api/swarming/v1/bot/%s/terminate' % args[0]
maruel0eb1d1b2015-10-02 14:48:21 -07001806 request = net.url_read_json(url, data={})
1807 if not request:
1808 print >> sys.stderr, 'Failed to ask for termination'
1809 return 1
1810 if options.wait:
1811 return collect(
Tim 'mithro' Ansell5e8001d2017-09-08 09:32:52 +10001812 options.swarming,
1813 [request['task_id']],
1814 0.,
1815 False,
1816 False,
1817 None,
1818 None,
1819 [],
Takuto Ikuta1e6072c2018-11-06 20:42:43 +00001820 False,
1821 None)
maruelbfc5f872017-06-10 16:43:17 -07001822 else:
1823 print request['task_id']
maruel0eb1d1b2015-10-02 14:48:21 -07001824 return 0
1825
1826
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001827@subcommand.usage("(hash|isolated) [-- extra_args|raw command]")
maruel@chromium.org0437a732013-08-27 16:05:52 +00001828def CMDtrigger(parser, args):
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001829 """Triggers a Swarming task.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001830
Vadim Shtayuraae8085b2014-05-02 17:13:10 -07001831 Passes all extra arguments provided after '--' as additional command line
1832 arguments for an isolated command specified in *.isolate file.
maruel@chromium.org0437a732013-08-27 16:05:52 +00001833 """
1834 add_trigger_options(parser)
Vadim Shtayurab450c602014-05-12 19:23:25 -07001835 add_sharding_options(parser)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001836 parser.add_option(
1837 '--dump-json',
1838 metavar='FILE',
1839 help='Dump details about the triggered task(s) to this file as json')
Marc-Antoine Ruel7c543272013-11-26 13:26:15 -05001840 options, args = parser.parse_args(args)
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001841 task_request = process_trigger_options(parser, options, args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001842 try:
Marc-Antoine Ruelefdc5282014-12-12 19:31:00 -05001843 tasks = trigger_task_shards(
1844 options.swarming, task_request, options.shards)
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001845 if tasks:
maruel0a25f6c2017-05-10 10:43:23 -07001846 print('Triggered task: %s' % task_request.name)
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001847 tasks_sorted = sorted(
1848 tasks.itervalues(), key=lambda x: x['shard_index'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001849 if options.dump_json:
1850 data = {
maruel0a25f6c2017-05-10 10:43:23 -07001851 'base_task_name': task_request.name,
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001852 'tasks': tasks,
Vadim Shtayura2d83a942017-08-14 17:41:24 -07001853 'request': task_request_to_raw_request(task_request),
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001854 }
maruel46b015f2015-10-13 18:40:35 -07001855 tools.write_json(unicode(options.dump_json), data, True)
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001856 print('To collect results, use:')
Bruce Dawsonf0a5ae42018-09-04 20:06:46 +00001857 print(' tools/swarming_client/swarming.py collect -S %s --json %s' %
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001858 (options.swarming, options.dump_json))
1859 else:
Marc-Antoine Ruel12a7da42014-10-01 08:29:47 -04001860 print('To collect results, use:')
Bruce Dawsonf0a5ae42018-09-04 20:06:46 +00001861 print(' tools/swarming_client/swarming.py collect -S %s %s' %
Marc-Antoine Ruel2f6581a2014-10-03 11:09:53 -04001862 (options.swarming, ' '.join(t['task_id'] for t in tasks_sorted)))
1863 print('Or visit:')
1864 for t in tasks_sorted:
1865 print(' ' + t['view_url'])
Marc-Antoine Rueld6dbe762014-06-18 13:49:42 -04001866 return int(not tasks)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001867 except Failure:
1868 on_error.report(None)
vadimsh@chromium.orgd908a542013-10-30 01:36:17 +00001869 return 1
maruel@chromium.org0437a732013-08-27 16:05:52 +00001870
1871
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001872class OptionParserSwarming(logging_utils.OptionParserWithLogging):
maruel@chromium.org0437a732013-08-27 16:05:52 +00001873 def __init__(self, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001874 logging_utils.OptionParserWithLogging.__init__(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001875 self, prog='swarming.py', **kwargs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001876 self.server_group = optparse.OptionGroup(self, 'Server')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001877 self.server_group.add_option(
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001878 '-S', '--swarming',
Kevin Graney5346c162014-01-24 12:20:01 -05001879 metavar='URL', default=os.environ.get('SWARMING_SERVER', ''),
maruel@chromium.orge9403ab2013-09-20 18:03:49 +00001880 help='Swarming server to use')
Marc-Antoine Ruel5471e3d2013-11-11 19:10:32 -05001881 self.add_option_group(self.server_group)
Vadim Shtayurae34e13a2014-02-02 11:23:26 -08001882 auth.add_auth_options(self)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001883
1884 def parse_args(self, *args, **kwargs):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001885 options, args = logging_utils.OptionParserWithLogging.parse_args(
maruel@chromium.org0437a732013-08-27 16:05:52 +00001886 self, *args, **kwargs)
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001887 auth.process_auth_options(self, options)
1888 user = self._process_swarming(options)
1889 if hasattr(options, 'user') and not options.user:
1890 options.user = user
1891 return options, args
1892
1893 def _process_swarming(self, options):
1894 """Processes the --swarming option and aborts if not specified.
1895
1896 Returns the identity as determined by the server.
1897 """
maruel@chromium.org0437a732013-08-27 16:05:52 +00001898 if not options.swarming:
1899 self.error('--swarming is required.')
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001900 try:
1901 options.swarming = net.fix_url(options.swarming)
1902 except ValueError as e:
1903 self.error('--swarming %s' % e)
1904 on_error.report_on_exception_exit(options.swarming)
Marc-Antoine Ruelf7d737d2014-12-10 15:36:29 -05001905 try:
1906 user = auth.ensure_logged_in(options.swarming)
1907 except ValueError as e:
1908 self.error(str(e))
Marc-Antoine Ruel012067b2014-12-10 15:45:42 -05001909 return user
maruel@chromium.org0437a732013-08-27 16:05:52 +00001910
1911
1912def main(args):
1913 dispatcher = subcommand.CommandDispatcher(__name__)
Marc-Antoine Ruelcfb60852014-07-02 15:22:00 -04001914 return dispatcher.execute(OptionParserSwarming(version=__version__), args)
maruel@chromium.org0437a732013-08-27 16:05:52 +00001915
1916
1917if __name__ == '__main__':
maruel8e4e40c2016-05-30 06:21:07 -07001918 subprocess42.inhibit_os_error_reporting()
maruel@chromium.org0437a732013-08-27 16:05:52 +00001919 fix_encoding.fix_encoding()
1920 tools.disable_buffering()
1921 colorama.init()
1922 sys.exit(main(sys.argv[1:]))