Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # Copyright 2013 The Swarming Authors. All rights reserved. | 2 # Copyright 2013 The Swarming Authors. All rights reserved. |
| 3 # Use of this source code is governed under the Apache License, Version 2.0 that | 3 # Use of this source code is governed under the Apache License, Version 2.0 that |
| 4 # can be found in the LICENSE file. | 4 # can be found in the LICENSE file. |
| 5 | 5 |
| 6 """Client tool to trigger tasks or retrieve results from a Swarming server.""" | 6 """Client tool to trigger tasks or retrieve results from a Swarming server.""" |
| 7 | 7 |
| 8 __version__ = '0.6.3' | 8 __version__ = '0.8.2' |
| 9 | 9 |
| 10 import collections | 10 import collections |
| 11 import datetime | 11 import datetime |
| 12 import json | 12 import json |
| 13 import logging | 13 import logging |
| 14 import optparse | 14 import optparse |
| 15 import os | 15 import os |
| 16 import re | |
| 17 import shutil | |
| 18 import StringIO | |
| 19 import subprocess | 16 import subprocess |
| 20 import sys | 17 import sys |
| 21 import threading | 18 import threading |
| 22 import time | 19 import time |
| 23 import urllib | 20 import urllib |
| 24 import urlparse | |
| 25 import zipfile | |
| 26 | 21 |
| 27 from third_party import colorama | 22 from third_party import colorama |
| 28 from third_party.depot_tools import fix_encoding | 23 from third_party.depot_tools import fix_encoding |
| 29 from third_party.depot_tools import subcommand | 24 from third_party.depot_tools import subcommand |
| 30 | 25 |
| 31 from utils import file_path | 26 from utils import file_path |
| 32 from utils import logging_utils | 27 from utils import logging_utils |
| 33 from third_party.chromium import natsort | 28 from third_party.chromium import natsort |
| 34 from utils import net | 29 from utils import net |
| 35 from utils import on_error | 30 from utils import on_error |
| 36 from utils import threading_utils | 31 from utils import threading_utils |
| 37 from utils import tools | 32 from utils import tools |
| 38 from utils import zip_package | |
| 39 | 33 |
| 40 import auth | 34 import auth |
| 41 import isolated_format | 35 import isolated_format |
| 42 import isolateserver | 36 import isolateserver |
| 43 import run_isolated | |
| 44 | 37 |
| 45 | 38 |
| 46 ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) | 39 ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) |
| 47 | 40 |
| 48 | 41 |
| 49 class Failure(Exception): | 42 class Failure(Exception): |
| 50 """Generic failure.""" | 43 """Generic failure.""" |
| 51 pass | 44 pass |
| 52 | 45 |
| 53 | 46 |
| 54 ### Isolated file handling. | 47 ### Isolated file handling. |
| 55 | 48 |
| 56 | 49 |
| 57 def isolated_upload_zip_bundle(isolate_server, bundle): | 50 def isolated_to_hash(arg, algo): |
| 58 """Uploads a zip package to Isolate Server and returns raw fetch URL. | |
| 59 | |
| 60 Args: | |
| 61 isolate_server: URL of an Isolate Server. | |
| 62 bundle: instance of ZipPackage to upload. | |
| 63 | |
| 64 Returns: | |
| 65 URL to get the file from. | |
| 66 """ | |
| 67 # Swarming bot needs to be able to grab the file from the Isolate Server using | |
| 68 # a simple HTTPS GET. Use 'default' namespace so that the raw data returned to | |
| 69 # a bot is not zipped, since the swarming_bot doesn't understand compressed | |
| 70 # data. This namespace have nothing to do with |namespace| passed to | |
| 71 # run_isolated.py that is used to store files for isolated task. | |
| 72 logging.info('Zipping up and uploading files...') | |
| 73 start_time = time.time() | |
| 74 isolate_item = isolateserver.BufferItem(bundle.zip_into_buffer()) | |
| 75 with isolateserver.get_storage(isolate_server, 'default') as storage: | |
| 76 uploaded = storage.upload_items([isolate_item]) | |
| 77 bundle_url = storage.get_fetch_url(isolate_item) | |
| 78 elapsed = time.time() - start_time | |
| 79 if isolate_item in uploaded: | |
| 80 logging.info('Upload complete, time elapsed: %f', elapsed) | |
| 81 else: | |
| 82 logging.info('Zip file already on server, time elapsed: %f', elapsed) | |
| 83 return bundle_url | |
| 84 | |
| 85 | |
| 86 def isolated_get_data(isolate_server): | |
| 87 """Returns the 'data' section with all files necessary to bootstrap a task | |
| 88 execution running an isolated task. | |
| 89 | |
| 90 It's mainly zipping run_isolated.zip over and over again. | |
| 91 TODO(maruel): Get rid of this with. | |
| 92 https://code.google.com/p/swarming/issues/detail?id=173 | |
| 93 """ | |
| 94 bundle = zip_package.ZipPackage(ROOT_DIR) | |
| 95 bundle.add_buffer( | |
| 96 'run_isolated.zip', | |
| 97 run_isolated.get_as_zip_package().zip_into_buffer(compress=False)) | |
| 98 bundle_url = isolated_upload_zip_bundle(isolate_server, bundle) | |
| 99 return [(bundle_url, 'swarm_data.zip')] | |
| 100 | |
| 101 | |
| 102 def isolated_get_run_commands( | |
| 103 isolate_server, namespace, isolated_hash, extra_args, verbose): | |
| 104 """Returns the 'commands' to run an isolated task via run_isolated.zip. | |
| 105 | |
| 106 Returns: | |
| 107 commands list to be added to the request. | |
| 108 """ | |
| 109 run_cmd = [ | |
| 110 'python', 'run_isolated.zip', | |
| 111 '--isolated', isolated_hash, | |
| 112 '--isolate-server', isolate_server, | |
| 113 '--namespace', namespace, | |
| 114 ] | |
| 115 if verbose: | |
| 116 run_cmd.append('--verbose') | |
| 117 # Pass all extra args for run_isolated.py, it will pass them to the command. | |
| 118 if extra_args: | |
| 119 run_cmd.append('--') | |
| 120 run_cmd.extend(extra_args) | |
| 121 return run_cmd | |
| 122 | |
| 123 | |
| 124 def isolated_archive(isolate_server, namespace, isolated, algo, verbose): | |
| 125 """Archives a .isolated and all the dependencies on the Isolate Server.""" | |
| 126 logging.info( | |
| 127 'isolated_archive(%s, %s, %s)', isolate_server, namespace, isolated) | |
| 128 print('Archiving: %s' % isolated) | |
| 129 cmd = [ | |
| 130 sys.executable, | |
| 131 os.path.join(ROOT_DIR, 'isolate.py'), | |
| 132 'archive', | |
| 133 '--isolate-server', isolate_server, | |
| 134 '--namespace', namespace, | |
| 135 '--isolated', isolated, | |
| 136 ] | |
| 137 cmd.extend(['--verbose'] * verbose) | |
| 138 logging.info(' '.join(cmd)) | |
| 139 if subprocess.call(cmd, verbose): | |
| 140 return None | |
| 141 return isolated_format.hash_file(isolated, algo) | |
| 142 | |
| 143 | |
| 144 def isolated_to_hash(isolate_server, namespace, arg, algo, verbose): | |
| 145 """Archives a .isolated file if needed. | 51 """Archives a .isolated file if needed. |
| 146 | 52 |
| 147 Returns the file hash to trigger and a bool specifying if it was a file (True) | 53 Returns the file hash to trigger and a bool specifying if it was a file (True) |
| 148 or a hash (False). | 54 or a hash (False). |
| 149 """ | 55 """ |
| 150 if arg.endswith('.isolated'): | 56 if arg.endswith('.isolated'): |
| 151 file_hash = isolated_archive(isolate_server, namespace, arg, algo, verbose) | 57 file_hash = isolated_format.hash_file(arg, algo) |
| 152 if not file_hash: | 58 if not file_hash: |
| 153 on_error.report('Archival failure %s' % arg) | 59 on_error.report('Archival failure %s' % arg) |
| 154 return None, True | 60 return None, True |
| 155 return file_hash, True | 61 return file_hash, True |
| 156 elif isolated_format.is_valid_hash(arg, algo): | 62 elif isolated_format.is_valid_hash(arg, algo): |
| 157 return arg, False | 63 return arg, False |
| 158 else: | 64 else: |
| 159 on_error.report('Invalid hash %s' % arg) | 65 on_error.report('Invalid hash %s' % arg) |
| 160 return None, False | 66 return None, False |
| 161 | 67 |
| 162 | 68 |
| 163 def isolated_handle_options(options, args): | 69 def isolated_handle_options(options, args): |
| 164 """Handles '--isolated <isolated>', '<isolated>' and '-- <args...>' arguments. | 70 """Handles '--isolated <isolated>', '<isolated>' and '-- <args...>' arguments. |
| 165 | 71 |
| 166 Returns: | 72 Returns: |
| 167 tuple(command, data). | 73 tuple(command, inputs_ref). |
| 168 """ | 74 """ |
| 169 isolated_cmd_args = [] | 75 isolated_cmd_args = [] |
| 170 if not options.isolated: | 76 if not options.isolated: |
| 171 if '--' in args: | 77 if '--' in args: |
| 172 index = args.index('--') | 78 index = args.index('--') |
| 173 isolated_cmd_args = args[index+1:] | 79 isolated_cmd_args = args[index+1:] |
| 174 args = args[:index] | 80 args = args[:index] |
| 175 else: | 81 else: |
| 176 # optparse eats '--' sometimes. | 82 # optparse eats '--' sometimes. |
| 177 isolated_cmd_args = args[1:] | 83 isolated_cmd_args = args[1:] |
| 178 args = args[:1] | 84 args = args[:1] |
| 179 if len(args) != 1: | 85 if len(args) != 1: |
| 180 raise ValueError( | 86 raise ValueError( |
| 181 'Use --isolated, --raw-cmd or \'--\' to pass arguments to the called ' | 87 'Use --isolated, --raw-cmd or \'--\' to pass arguments to the called ' |
| 182 'process.') | 88 'process.') |
| 183 # Old code. To be removed eventually. | 89 # Old code. To be removed eventually. |
| 184 options.isolated, is_file = isolated_to_hash( | 90 options.isolated, is_file = isolated_to_hash( |
| 185 options.isolate_server, options.namespace, args[0], | 91 args[0], isolated_format.get_hash_algo(options.namespace)) |
| 186 isolated_format.get_hash_algo(options.namespace), options.verbose) | |
| 187 if not options.isolated: | 92 if not options.isolated: |
| 188 raise ValueError('Invalid argument %s' % args[0]) | 93 raise ValueError('Invalid argument %s' % args[0]) |
| 189 elif args: | 94 elif args: |
| 190 is_file = False | 95 is_file = False |
| 191 if '--' in args: | 96 if '--' in args: |
| 192 index = args.index('--') | 97 index = args.index('--') |
| 193 isolated_cmd_args = args[index+1:] | 98 isolated_cmd_args = args[index+1:] |
| 194 if index != 0: | 99 if index != 0: |
| 195 raise ValueError('Unexpected arguments.') | 100 raise ValueError('Unexpected arguments.') |
| 196 else: | 101 else: |
| 197 # optparse eats '--' sometimes. | 102 # optparse eats '--' sometimes. |
| 198 isolated_cmd_args = args | 103 isolated_cmd_args = args |
| 199 | 104 |
| 200 command = isolated_get_run_commands( | |
| 201 options.isolate_server, options.namespace, options.isolated, | |
| 202 isolated_cmd_args, options.verbose) | |
| 203 | |
| 204 # If a file name was passed, use its base name of the isolated hash. | 105 # If a file name was passed, use its base name of the isolated hash. |
| 205 # Otherwise, use user name as an approximation of a task name. | 106 # Otherwise, use user name as an approximation of a task name. |
| 206 if not options.task_name: | 107 if not options.task_name: |
| 207 if is_file: | 108 if is_file: |
| 208 key = os.path.splitext(os.path.basename(args[0]))[0] | 109 key = os.path.splitext(os.path.basename(args[0]))[0] |
| 209 else: | 110 else: |
| 210 key = options.user | 111 key = options.user |
| 211 options.task_name = u'%s/%s/%s' % ( | 112 options.task_name = u'%s/%s/%s' % ( |
| 212 key, | 113 key, |
| 213 '_'.join( | 114 '_'.join( |
| 214 '%s=%s' % (k, v) | 115 '%s=%s' % (k, v) |
| 215 for k, v in sorted(options.dimensions.iteritems())), | 116 for k, v in sorted(options.dimensions.iteritems())), |
| 216 options.isolated) | 117 options.isolated) |
| 217 | 118 |
| 218 try: | 119 inputs_ref = FilesRef( |
| 219 data = isolated_get_data(options.isolate_server) | 120 isolated=options.isolated, |
| 220 except (IOError, OSError): | 121 isolatedserver=options.isolate_server, |
| 221 on_error.report('Failed to upload the zip file') | 122 namespace=options.namespace) |
| 222 raise ValueError('Failed to upload the zip file') | 123 return isolated_cmd_args, inputs_ref |
| 223 | |
| 224 return command, data | |
| 225 | 124 |
| 226 | 125 |
| 227 ### Triggering. | 126 ### Triggering. |
| 228 | 127 |
| 229 | 128 |
| 230 TaskRequest = collections.namedtuple( | 129 # See ../appengine/swarming/swarming_rpcs.py. |
| 231 'TaskRequest', | 130 FilesRef = collections.namedtuple( |
| 131 'FilesRef', | |
| 132 [ | |
| 133 'isolated', | |
| 134 'isolatedserver', | |
| 135 'namespace', | |
| 136 ]) | |
| 137 | |
| 138 | |
| 139 # See ../appengine/swarming/swarming_rpcs.py. | |
| 140 TaskProperties = collections.namedtuple( | |
| 141 'TaskProperties', | |
| 232 [ | 142 [ |
| 233 'command', | 143 'command', |
| 234 'data', | |
| 235 'dimensions', | 144 'dimensions', |
| 236 'env', | 145 'env', |
| 237 'expiration', | 146 'execution_timeout_secs', |
| 238 'hard_timeout', | 147 'extra_args', |
| 148 'grace_period_secs', | |
| 239 'idempotent', | 149 'idempotent', |
| 240 'io_timeout', | 150 'inputs_ref', |
| 151 'io_timeout_secs', | |
| 152 ]) | |
| 153 | |
| 154 | |
| 155 # See ../appengine/swarming/swarming_rpcs.py. | |
| 156 NewTaskRequest = collections.namedtuple( | |
| 157 'NewTaskRequest', | |
| 158 [ | |
| 159 'expiration_secs', | |
| 241 'name', | 160 'name', |
| 161 'parent_task_id', | |
| 242 'priority', | 162 'priority', |
| 163 'properties', | |
| 243 'tags', | 164 'tags', |
| 244 'user', | 165 'user', |
| 245 'verbose', | |
| 246 ]) | 166 ]) |
| 247 | 167 |
| 248 | 168 |
| 169 def namedtuple_to_dict(value): | |
| 170 """Recursively converts a namedtuple to a dict.""" | |
| 171 out = dict(value._asdict()) | |
| 172 for k, v in out.iteritems(): | |
| 173 if hasattr(v, '_asdict'): | |
| 174 out[k] = namedtuple_to_dict(v) | |
| 175 return out | |
| 176 | |
| 177 | |
| 249 def task_request_to_raw_request(task_request): | 178 def task_request_to_raw_request(task_request): |
| 250 """Returns the json dict expected by the Swarming server for new request. | 179 """Returns the json dict expected by the Swarming server for new request. |
| 251 | 180 |
| 252 This is for the v1 client Swarming API. | 181 This is for the v1 client Swarming API. |
| 253 """ | 182 """ |
| 254 return { | 183 out = namedtuple_to_dict(task_request) |
| 255 'name': task_request.name, | 184 # Maps are not supported until protobuf v3. |
| 256 'parent_task_id': os.environ.get('SWARMING_TASK_ID', ''), | 185 out['properties']['dimensions'] = [ |
| 257 'priority': task_request.priority, | 186 {'key': k, 'value': v} |
| 258 'properties': { | 187 for k, v in out['properties']['dimensions'].iteritems() |
| 259 'commands': [task_request.command], | 188 ] |
| 260 'data': task_request.data, | 189 out['properties']['dimensions'].sort(key=lambda x: x['key']) |
| 261 'dimensions': task_request.dimensions, | 190 out['properties']['env'] = [ |
| 262 'env': task_request.env, | 191 {'key': k, 'value': v} |
| 263 'execution_timeout_secs': task_request.hard_timeout, | 192 for k, v in out['properties']['env'].iteritems() |
| 264 'io_timeout_secs': task_request.io_timeout, | 193 ] |
| 265 'idempotent': task_request.idempotent, | 194 out['properties']['env'].sort(key=lambda x: x['key']) |
| 266 }, | 195 return out |
| 267 'scheduling_expiration_secs': task_request.expiration, | |
| 268 'tags': task_request.tags, | |
| 269 'user': task_request.user, | |
| 270 } | |
| 271 | 196 |
| 272 | 197 |
| 273 def swarming_handshake(swarming): | 198 def swarming_trigger(swarming, raw_request): |
| 274 """Initiates the connection to the Swarming server.""" | |
| 275 headers = {'X-XSRF-Token-Request': '1'} | |
| 276 response = net.url_read_json( | |
| 277 swarming + '/swarming/api/v1/client/handshake', | |
| 278 headers=headers, | |
| 279 data={}) | |
| 280 if not response: | |
| 281 logging.error('Failed to handshake with server') | |
| 282 return None | |
| 283 logging.info('Connected to server version: %s', response['server_version']) | |
| 284 return response['xsrf_token'] | |
| 285 | |
| 286 | |
| 287 def swarming_trigger(swarming, raw_request, xsrf_token): | |
| 288 """Triggers a request on the Swarming server and returns the json data. | 199 """Triggers a request on the Swarming server and returns the json data. |
| 289 | 200 |
| 290 It's the low-level function. | 201 It's the low-level function. |
| 291 | 202 |
| 292 Returns: | 203 Returns: |
| 293 { | 204 { |
| 294 'request': { | 205 'request': { |
| 295 'created_ts': u'2010-01-02 03:04:05', | 206 'created_ts': u'2010-01-02 03:04:05', |
| 296 'name': .. | 207 'name': .. |
| 297 }, | 208 }, |
| 298 'task_id': '12300', | 209 'task_id': '12300', |
| 299 } | 210 } |
| 300 """ | 211 """ |
| 301 logging.info('Triggering: %s', raw_request['name']) | 212 logging.info('Triggering: %s', raw_request['name']) |
| 302 | 213 |
| 303 headers = {'X-XSRF-Token': xsrf_token} | |
| 304 result = net.url_read_json( | 214 result = net.url_read_json( |
| 305 swarming + '/swarming/api/v1/client/request', | 215 swarming + '/_ah/api/swarming/v1/tasks/new', data=raw_request) |
| 306 data=raw_request, | |
| 307 headers=headers) | |
| 308 if not result: | 216 if not result: |
| 309 on_error.report('Failed to trigger task %s' % raw_request['name']) | 217 on_error.report('Failed to trigger task %s' % raw_request['name']) |
| 310 return None | 218 return None |
| 311 return result | 219 return result |
| 312 | 220 |
| 313 | 221 |
| 314 def setup_googletest(env, shards, index): | 222 def setup_googletest(env, shards, index): |
| 315 """Sets googletest specific environment variables.""" | 223 """Sets googletest specific environment variables.""" |
| 316 if shards > 1: | 224 if shards > 1: |
| 317 env = env.copy() | 225 env = env[:] |
| 318 env['GTEST_SHARD_INDEX'] = str(index) | 226 env.append({'key': 'GTEST_SHARD_INDEX', 'value': str(index)}) |
|
Vadim Sh.
2015/09/14 17:46:52
nit: override existing GTEST_SHARD_INDEX and GTEST
M-A Ruel
2015/09/15 14:13:54
Added asserts that they are not present instead. I
| |
| 319 env['GTEST_TOTAL_SHARDS'] = str(shards) | 227 env.append({'key': 'GTEST_TOTAL_SHARDS', 'value': str(shards)}) |
| 320 return env | 228 return env |
| 321 | 229 |
| 322 | 230 |
| 323 def trigger_task_shards(swarming, task_request, shards): | 231 def trigger_task_shards(swarming, task_request, shards): |
| 324 """Triggers one or many subtasks of a sharded task. | 232 """Triggers one or many subtasks of a sharded task. |
| 325 | 233 |
| 326 Returns: | 234 Returns: |
| 327 Dict with task details, returned to caller as part of --dump-json output. | 235 Dict with task details, returned to caller as part of --dump-json output. |
| 328 None in case of failure. | 236 None in case of failure. |
| 329 """ | 237 """ |
| 330 def convert(index): | 238 def convert(index): |
| 331 req = task_request | 239 req = task_request_to_raw_request(task_request) |
| 332 if shards > 1: | 240 if shards > 1: |
| 333 req = req._replace( | 241 req['properties']['env'] = setup_googletest( |
| 334 env=setup_googletest(req.env, shards, index), | 242 req['properties']['env'], shards, index) |
| 335 name='%s:%s:%s' % (req.name, index, shards)) | 243 req['name'] += ':%s:%s' % (index, shards) |
| 336 return task_request_to_raw_request(req) | 244 return req |
| 337 | 245 |
| 338 requests = [convert(index) for index in xrange(shards)] | 246 requests = [convert(index) for index in xrange(shards)] |
| 339 xsrf_token = swarming_handshake(swarming) | |
| 340 if not xsrf_token: | |
| 341 return None | |
| 342 tasks = {} | 247 tasks = {} |
| 343 priority_warning = False | 248 priority_warning = False |
| 344 for index, request in enumerate(requests): | 249 for index, request in enumerate(requests): |
| 345 task = swarming_trigger(swarming, request, xsrf_token) | 250 task = swarming_trigger(swarming, request) |
| 346 if not task: | 251 if not task: |
| 347 break | 252 break |
| 348 logging.info('Request result: %s', task) | 253 logging.info('Request result: %s', task) |
| 349 if (not priority_warning and | 254 if (not priority_warning and |
| 350 task['request']['priority'] != task_request.priority): | 255 task['request']['priority'] != task_request.priority): |
| 351 priority_warning = True | 256 priority_warning = True |
| 352 print >> sys.stderr, ( | 257 print >> sys.stderr, ( |
| 353 'Priority was reset to %s' % task['request']['priority']) | 258 'Priority was reset to %s' % task['request']['priority']) |
| 354 tasks[request['name']] = { | 259 tasks[request['name']] = { |
| 355 'shard_index': index, | 260 'shard_index': index, |
| (...skipping 29 matching lines...) Expand all Loading... | |
| 385 It's in fact an enum. Values should be in decreasing order of importance. | 290 It's in fact an enum. Values should be in decreasing order of importance. |
| 386 """ | 291 """ |
| 387 RUNNING = 0x10 | 292 RUNNING = 0x10 |
| 388 PENDING = 0x20 | 293 PENDING = 0x20 |
| 389 EXPIRED = 0x30 | 294 EXPIRED = 0x30 |
| 390 TIMED_OUT = 0x40 | 295 TIMED_OUT = 0x40 |
| 391 BOT_DIED = 0x50 | 296 BOT_DIED = 0x50 |
| 392 CANCELED = 0x60 | 297 CANCELED = 0x60 |
| 393 COMPLETED = 0x70 | 298 COMPLETED = 0x70 |
| 394 | 299 |
| 395 STATES = (RUNNING, PENDING, EXPIRED, TIMED_OUT, BOT_DIED, CANCELED, COMPLETED) | 300 STATES = ( |
| 396 STATES_RUNNING = (RUNNING, PENDING) | 301 'RUNNING', 'PENDING', 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED', |
| 397 STATES_NOT_RUNNING = (EXPIRED, TIMED_OUT, BOT_DIED, CANCELED, COMPLETED) | 302 'COMPLETED') |
| 398 STATES_DONE = (TIMED_OUT, COMPLETED) | 303 STATES_RUNNING = ('RUNNING', 'PENDING') |
| 399 STATES_ABANDONED = (EXPIRED, BOT_DIED, CANCELED) | 304 STATES_NOT_RUNNING = ( |
| 305 'EXPIRED', 'TIMED_OUT', 'BOT_DIED', 'CANCELED', 'COMPLETED') | |
| 306 STATES_DONE = ('TIMED_OUT', 'COMPLETED') | |
| 307 STATES_ABANDONED = ('EXPIRED', 'BOT_DIED', 'CANCELED') | |
| 400 | 308 |
| 401 _NAMES = { | 309 _NAMES = { |
| 402 RUNNING: 'Running', | 310 RUNNING: 'Running', |
| 403 PENDING: 'Pending', | 311 PENDING: 'Pending', |
| 404 EXPIRED: 'Expired', | 312 EXPIRED: 'Expired', |
| 405 TIMED_OUT: 'Execution timed out', | 313 TIMED_OUT: 'Execution timed out', |
| 406 BOT_DIED: 'Bot died', | 314 BOT_DIED: 'Bot died', |
| 407 CANCELED: 'User canceled', | 315 CANCELED: 'User canceled', |
| 408 COMPLETED: 'Completed', | 316 COMPLETED: 'Completed', |
| 409 } | 317 } |
| 410 | 318 |
| 319 _ENUMS = { | |
| 320 'RUNNING': RUNNING, | |
| 321 'PENDING': PENDING, | |
| 322 'EXPIRED': EXPIRED, | |
| 323 'TIMED_OUT': TIMED_OUT, | |
| 324 'BOT_DIED': BOT_DIED, | |
| 325 'CANCELED': CANCELED, | |
| 326 'COMPLETED': COMPLETED, | |
| 327 } | |
| 328 | |
| 411 @classmethod | 329 @classmethod |
| 412 def to_string(cls, state): | 330 def to_string(cls, state): |
| 413 """Returns a user-readable string representing a State.""" | 331 """Returns a user-readable string representing a State.""" |
| 414 if state not in cls._NAMES: | 332 if state not in cls._NAMES: |
| 415 raise ValueError('Invalid state %s' % state) | 333 raise ValueError('Invalid state %s' % state) |
| 416 return cls._NAMES[state] | 334 return cls._NAMES[state] |
| 417 | 335 |
| 336 @classmethod | |
| 337 def from_enum(cls, state): | |
| 338 """Returns int value based on the string.""" | |
| 339 if state not in cls._ENUMS: | |
| 340 raise ValueError('Invalid state %s' % state) | |
| 341 return cls._ENUMS[state] | |
| 342 | |
| 418 | 343 |
| 419 class TaskOutputCollector(object): | 344 class TaskOutputCollector(object): |
| 420 """Assembles task execution summary (for --task-summary-json output). | 345 """Assembles task execution summary (for --task-summary-json output). |
| 421 | 346 |
| 422 Optionally fetches task outputs from isolate server to local disk (used when | 347 Optionally fetches task outputs from isolate server to local disk (used when |
| 423 --task-output-dir is passed). | 348 --task-output-dir is passed). |
| 424 | 349 |
| 425 This object is shared among multiple threads running 'retrieve_results' | 350 This object is shared among multiple threads running 'retrieve_results' |
| 426 function, in particular they call 'process_shard_result' method in parallel. | 351 function, in particular they call 'process_shard_result' method in parallel. |
| 427 """ | 352 """ |
| (...skipping 15 matching lines...) Expand all Loading... | |
| 443 self._storage = None | 368 self._storage = None |
| 444 | 369 |
| 445 if self.task_output_dir and not os.path.isdir(self.task_output_dir): | 370 if self.task_output_dir and not os.path.isdir(self.task_output_dir): |
| 446 os.makedirs(self.task_output_dir) | 371 os.makedirs(self.task_output_dir) |
| 447 | 372 |
| 448 def process_shard_result(self, shard_index, result): | 373 def process_shard_result(self, shard_index, result): |
| 449 """Stores results of a single task shard, fetches output files if necessary. | 374 """Stores results of a single task shard, fetches output files if necessary. |
| 450 | 375 |
| 451 Modifies |result| in place. | 376 Modifies |result| in place. |
| 452 | 377 |
| 378 shard_index is 0-based. | |
| 379 | |
| 453 Called concurrently from multiple threads. | 380 Called concurrently from multiple threads. |
| 454 """ | 381 """ |
| 455 # Sanity check index is in expected range. | 382 # Sanity check index is in expected range. |
| 456 assert isinstance(shard_index, int) | 383 assert isinstance(shard_index, int) |
| 457 if shard_index < 0 or shard_index >= self.shard_count: | 384 if shard_index < 0 or shard_index >= self.shard_count: |
| 458 logging.warning( | 385 logging.warning( |
| 459 'Shard index %d is outside of expected range: [0; %d]', | 386 'Shard index %d is outside of expected range: [0; %d]', |
| 460 shard_index, self.shard_count - 1) | 387 shard_index, self.shard_count - 1) |
| 461 return | 388 return |
| 462 | 389 |
| 463 assert not 'isolated_out' in result | 390 if result.get('outputs_ref'): |
| 464 result['isolated_out'] = None | 391 ref = result['outputs_ref'] |
| 465 for output in result['outputs']: | 392 result['outputs_ref']['view_url'] = '%s/browse?%s' % ( |
| 466 isolated_files_location = extract_output_files_location(output) | 393 ref['isolatedserver'], |
| 467 if isolated_files_location: | 394 urllib.urlencode( |
| 468 if result['isolated_out']: | 395 [('namespace', ref['namespace']), ('hash', ref['isolated'])])) |
| 469 raise ValueError('Unexpected two task with output') | |
| 470 result['isolated_out'] = isolated_files_location | |
| 471 | 396 |
| 472 # Store result dict of that shard, ignore results we've already seen. | 397 # Store result dict of that shard, ignore results we've already seen. |
| 473 with self._lock: | 398 with self._lock: |
| 474 if shard_index in self._per_shard_results: | 399 if shard_index in self._per_shard_results: |
| 475 logging.warning('Ignoring duplicate shard index %d', shard_index) | 400 logging.warning('Ignoring duplicate shard index %d', shard_index) |
| 476 return | 401 return |
| 477 self._per_shard_results[shard_index] = result | 402 self._per_shard_results[shard_index] = result |
| 478 | 403 |
| 479 # Fetch output files if necessary. | 404 # Fetch output files if necessary. |
| 480 if self.task_output_dir and result['isolated_out']: | 405 if self.task_output_dir and result.get('outputs_ref'): |
| 481 storage = self._get_storage( | 406 storage = self._get_storage( |
| 482 result['isolated_out']['server'], | 407 result['outputs_ref']['isolatedserver'], |
| 483 result['isolated_out']['namespace']) | 408 result['outputs_ref']['namespace']) |
| 484 if storage: | 409 if storage: |
| 485 # Output files are supposed to be small and they are not reused across | 410 # Output files are supposed to be small and they are not reused across |
| 486 # tasks. So use MemoryCache for them instead of on-disk cache. Make | 411 # tasks. So use MemoryCache for them instead of on-disk cache. Make |
| 487 # files writable, so that calling script can delete them. | 412 # files writable, so that calling script can delete them. |
| 488 isolateserver.fetch_isolated( | 413 isolateserver.fetch_isolated( |
| 489 result['isolated_out']['hash'], | 414 result['outputs_ref']['isolated'], |
| 490 storage, | 415 storage, |
| 491 isolateserver.MemoryCache(file_mode_mask=0700), | 416 isolateserver.MemoryCache(file_mode_mask=0700), |
| 492 os.path.join(self.task_output_dir, str(shard_index)), | 417 os.path.join(self.task_output_dir, str(shard_index)), |
| 493 False) | 418 False) |
| 494 | 419 |
| 495 def finalize(self): | 420 def finalize(self): |
| 496 """Assembles and returns task summary JSON, shutdowns underlying Storage.""" | 421 """Assembles and returns task summary JSON, shutdowns underlying Storage.""" |
| 497 with self._lock: | 422 with self._lock: |
| 498 # Write an array of shard results with None for missing shards. | 423 # Write an array of shard results with None for missing shards. |
| 499 summary = { | 424 summary = { |
| (...skipping 26 matching lines...) Expand all Loading... | |
| 526 self._storage.location, isolate_server) | 451 self._storage.location, isolate_server) |
| 527 return None | 452 return None |
| 528 if self._storage.namespace != namespace: | 453 if self._storage.namespace != namespace: |
| 529 logging.error( | 454 logging.error( |
| 530 'Task shards are using multiple namespaces: %s and %s', | 455 'Task shards are using multiple namespaces: %s and %s', |
| 531 self._storage.namespace, namespace) | 456 self._storage.namespace, namespace) |
| 532 return None | 457 return None |
| 533 return self._storage | 458 return self._storage |
| 534 | 459 |
| 535 | 460 |
| 536 def extract_output_files_location(task_log): | |
| 537 """Task log -> location of task output files to fetch. | |
| 538 | |
| 539 TODO(vadimsh,maruel): Use side-channel to get this information. | |
| 540 See 'run_tha_test' in run_isolated.py for where the data is generated. | |
| 541 | |
| 542 Returns: | |
| 543 Tuple (isolate server URL, namespace, isolated hash) on success. | |
| 544 None if information is missing or can not be parsed. | |
| 545 """ | |
| 546 if not task_log: | |
| 547 return None | |
| 548 match = re.search( | |
| 549 r'\[run_isolated_out_hack\](.*)\[/run_isolated_out_hack\]', | |
| 550 task_log, | |
| 551 re.DOTALL) | |
| 552 if not match: | |
| 553 return None | |
| 554 | |
| 555 def to_ascii(val): | |
| 556 if not isinstance(val, basestring): | |
| 557 raise ValueError() | |
| 558 return val.encode('ascii') | |
| 559 | |
| 560 try: | |
| 561 data = json.loads(match.group(1)) | |
| 562 if not isinstance(data, dict): | |
| 563 raise ValueError() | |
| 564 isolated_hash = to_ascii(data['hash']) | |
| 565 namespace = to_ascii(data['namespace']) | |
| 566 isolate_server = to_ascii(data['storage']) | |
| 567 if not file_path.is_url(isolate_server): | |
| 568 raise ValueError() | |
| 569 data = { | |
| 570 'hash': isolated_hash, | |
| 571 'namespace': namespace, | |
| 572 'server': isolate_server, | |
| 573 'view_url': '%s/browse?%s' % (isolate_server, urllib.urlencode( | |
| 574 [('namespace', namespace), ('hash', isolated_hash)])), | |
| 575 } | |
| 576 return data | |
| 577 except (KeyError, ValueError): | |
| 578 logging.warning( | |
| 579 'Unexpected value of run_isolated_out_hack: %s', match.group(1)) | |
| 580 return None | |
| 581 | |
| 582 | |
| 583 def now(): | 461 def now(): |
| 584 """Exists so it can be mocked easily.""" | 462 """Exists so it can be mocked easily.""" |
| 585 return time.time() | 463 return time.time() |
| 586 | 464 |
| 587 | 465 |
| 466 def parse_time(value): | |
| 467 """Converts serialized time from the API to datetime.datetime.""" | |
| 468 # When microseconds are 0, the '.123456' suffix is elided. This means the | |
| 469 # serialized format is not consistent, which confuses the hell out of python. | |
| 470 for fmt in ('%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S'): | |
| 471 try: | |
| 472 return datetime.datetime.strptime(value, fmt) | |
| 473 except ValueError: | |
| 474 pass | |
| 475 raise ValueError('Failed to parse %s' % value) | |
| 476 | |
| 477 | |
| 588 def retrieve_results( | 478 def retrieve_results( |
| 589 base_url, shard_index, task_id, timeout, should_stop, output_collector): | 479 base_url, shard_index, task_id, timeout, should_stop, output_collector): |
| 590 """Retrieves results for a single task ID. | 480 """Retrieves results for a single task ID. |
| 591 | 481 |
| 592 Returns: | 482 Returns: |
| 593 <result dict> on success. | 483 <result dict> on success. |
| 594 None on failure. | 484 None on failure. |
| 595 """ | 485 """ |
| 596 assert isinstance(timeout, float), timeout | 486 assert isinstance(timeout, float), timeout |
| 597 result_url = '%s/swarming/api/v1/client/task/%s' % (base_url, task_id) | 487 result_url = '%s/_ah/api/swarming/v1/task/%s/result' % (base_url, task_id) |
| 598 output_url = '%s/swarming/api/v1/client/task/%s/output/all' % ( | 488 output_url = '%s/_ah/api/swarming/v1/task/%s/stdout' % (base_url, task_id) |
| 599 base_url, task_id) | |
| 600 started = now() | 489 started = now() |
| 601 deadline = started + timeout if timeout else None | 490 deadline = started + timeout if timeout else None |
| 602 attempt = 0 | 491 attempt = 0 |
| 603 | 492 |
| 604 while not should_stop.is_set(): | 493 while not should_stop.is_set(): |
| 605 attempt += 1 | 494 attempt += 1 |
| 606 | 495 |
| 607 # Waiting for too long -> give up. | 496 # Waiting for too long -> give up. |
| 608 current_time = now() | 497 current_time = now() |
| 609 if deadline and current_time >= deadline: | 498 if deadline and current_time >= deadline: |
| (...skipping 12 matching lines...) Expand all Loading... | |
| 622 should_stop.wait(delay) | 511 should_stop.wait(delay) |
| 623 if should_stop.is_set(): | 512 if should_stop.is_set(): |
| 624 return None | 513 return None |
| 625 | 514 |
| 626 # Disable internal retries in net.url_read_json, since we are doing retries | 515 # Disable internal retries in net.url_read_json, since we are doing retries |
| 627 # ourselves. | 516 # ourselves. |
| 628 # TODO(maruel): We'd need to know if it's a 404 and not retry at all. | 517 # TODO(maruel): We'd need to know if it's a 404 and not retry at all. |
| 629 result = net.url_read_json(result_url, retry_50x=False) | 518 result = net.url_read_json(result_url, retry_50x=False) |
| 630 if not result: | 519 if not result: |
| 631 continue | 520 continue |
| 521 | |
| 632 if result['state'] in State.STATES_NOT_RUNNING: | 522 if result['state'] in State.STATES_NOT_RUNNING: |
| 523 # TODO(maruel): Not always fetch stdout? | |
| 633 out = net.url_read_json(output_url) | 524 out = net.url_read_json(output_url) |
| 634 result['outputs'] = (out or {}).get('outputs', []) | 525 result['output'] = out['output'] if out else out |
| 635 if not result['outputs']: | 526 if not result['output']: |
| 636 logging.error('No output found for task %s', task_id) | 527 logging.error('No output found for task %s', task_id) |
| 637 # Record the result, try to fetch attached output files (if any). | 528 # Record the result, try to fetch attached output files (if any). |
| 638 if output_collector: | 529 if output_collector: |
| 639 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching. | 530 # TODO(vadimsh): Respect |should_stop| and |deadline| when fetching. |
| 640 output_collector.process_shard_result(shard_index, result) | 531 output_collector.process_shard_result(shard_index, result) |
| 641 return result | 532 return result |
| 642 | 533 |
| 643 | 534 |
| 535 def convert_to_old_format(result): | |
| 536 """Converts the task result data from Endpoints API format to old API format | |
| 537 for compatibility. | |
| 538 | |
| 539 This goes into the file generated as --task-summary-json. | |
| 540 """ | |
| 541 # Sets default. | |
| 542 result.setdefault('abandoned_ts', None) | |
| 543 result.setdefault('bot_id', None) | |
| 544 result.setdefault('bot_version', None) | |
| 545 result.setdefault('children_task_ids', []) | |
| 546 result.setdefault('completed_ts', None) | |
| 547 result.setdefault('cost_saved_usd', None) | |
| 548 result.setdefault('costs_usd', None) | |
| 549 result.setdefault('deduped_from', None) | |
| 550 result.setdefault('name', None) | |
| 551 result.setdefault('outputs_ref', None) | |
| 552 result.setdefault('properties_hash', None) | |
| 553 result.setdefault('server_versions', None) | |
| 554 result.setdefault('started_ts', None) | |
| 555 result.setdefault('tags', None) | |
| 556 result.setdefault('user', None) | |
| 557 | |
| 558 # Convertion back to old API. | |
| 559 duration = result.pop('duration', None) | |
| 560 result['durations'] = [duration] if duration else [] | |
| 561 exit_code = result.pop('exit_code', None) | |
| 562 result['exit_codes'] = [int(exit_code)] if exit_code else [] | |
| 563 result['id'] = result.pop('task_id') | |
| 564 result['isolated_out'] = result.get('outputs_ref', None) | |
| 565 output = result.pop('output', None) | |
| 566 result['outputs'] = [output] if output else [] | |
| 567 # properties_hash | |
| 568 # server_version | |
| 569 # Endpoints result 'state' as string. For compatibility with old code, convert | |
| 570 # to int. | |
| 571 result['state'] = State.from_enum(result['state']) | |
| 572 # tags | |
| 573 result['try_number'] = ( | |
| 574 int(result['try_number']) if result['try_number'] else None) | |
| 575 result['bot_dimensions'] = { | |
| 576 i['key']: i['value'] for i in result['bot_dimensions'] | |
| 577 } | |
| 578 | |
| 579 | |
| 644 def yield_results( | 580 def yield_results( |
| 645 swarm_base_url, task_ids, timeout, max_threads, print_status_updates, | 581 swarm_base_url, task_ids, timeout, max_threads, print_status_updates, |
| 646 output_collector): | 582 output_collector): |
| 647 """Yields swarming task results from the swarming server as (index, result). | 583 """Yields swarming task results from the swarming server as (index, result). |
| 648 | 584 |
| 649 Duplicate shards are ignored. Shards are yielded in order of completion. | 585 Duplicate shards are ignored. Shards are yielded in order of completion. |
| 650 Timed out shards are NOT yielded at all. Caller can compare number of yielded | 586 Timed out shards are NOT yielded at all. Caller can compare number of yielded |
| 651 shards with len(task_keys) to verify all shards completed. | 587 shards with len(task_keys) to verify all shards completed. |
| 652 | 588 |
| 653 max_threads is optional and is used to limit the number of parallel fetches | 589 max_threads is optional and is used to limit the number of parallel fetches |
| (...skipping 55 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 709 shards_remaining.remove(shard_index) | 645 shards_remaining.remove(shard_index) |
| 710 yield shard_index, result | 646 yield shard_index, result |
| 711 | 647 |
| 712 finally: | 648 finally: |
| 713 # Done or aborted with Ctrl+C, kill the remaining threads. | 649 # Done or aborted with Ctrl+C, kill the remaining threads. |
| 714 should_stop.set() | 650 should_stop.set() |
| 715 | 651 |
| 716 | 652 |
| 717 def decorate_shard_output(swarming, shard_index, metadata): | 653 def decorate_shard_output(swarming, shard_index, metadata): |
| 718 """Returns wrapped output for swarming task shard.""" | 654 """Returns wrapped output for swarming task shard.""" |
| 719 def t(d): | 655 if metadata.get('started_ts') and not metadata.get('deduped_from'): |
| 720 return datetime.datetime.strptime(d, '%Y-%m-%d %H:%M:%S') | |
| 721 if metadata.get('started_ts'): | |
| 722 pending = '%.1fs' % ( | 656 pending = '%.1fs' % ( |
| 723 t(metadata['started_ts']) - t(metadata['created_ts'])).total_seconds() | 657 parse_time(metadata['started_ts']) - parse_time(metadata['created_ts']) |
| 658 ).total_seconds() | |
| 724 else: | 659 else: |
| 725 pending = 'N/A' | 660 pending = 'N/A' |
| 726 | 661 |
| 727 if metadata.get('durations'): | 662 if metadata.get('duration') is not None: |
| 728 duration = '%.1fs' % metadata['durations'][0] | 663 duration = '%.1fs' % metadata['duration'] |
| 729 else: | 664 else: |
| 730 duration = 'N/A' | 665 duration = 'N/A' |
| 731 | 666 |
| 732 if metadata.get('exit_codes'): | 667 if metadata.get('exit_code') is not None: |
| 733 exit_code = '%d' % metadata['exit_codes'][0] | 668 # Integers are encoded as string to not loose precision. |
| 669 exit_code = '%s' % metadata['exit_code'] | |
| 734 else: | 670 else: |
| 735 exit_code = 'N/A' | 671 exit_code = 'N/A' |
| 736 | 672 |
| 737 bot_id = metadata.get('bot_id') or 'N/A' | 673 bot_id = metadata.get('bot_id') or 'N/A' |
| 738 | 674 |
| 739 url = '%s/user/task/%s' % (swarming, metadata['id']) | 675 url = '%s/user/task/%s' % (swarming, metadata['task_id']) |
| 740 tag_header = 'Shard %d %s' % (shard_index, url) | 676 tag_header = 'Shard %d %s' % (shard_index, url) |
| 741 tag_footer = ( | 677 tag_footer = ( |
| 742 'End of shard %d Pending: %s Duration: %s Bot: %s Exit: %s' % ( | 678 'End of shard %d Pending: %s Duration: %s Bot: %s Exit: %s' % ( |
| 743 shard_index, pending, duration, bot_id, exit_code)) | 679 shard_index, pending, duration, bot_id, exit_code)) |
| 744 | 680 |
| 745 tag_len = max(len(tag_header), len(tag_footer)) | 681 tag_len = max(len(tag_header), len(tag_footer)) |
| 746 dash_pad = '+-%s-+\n' % ('-' * tag_len) | 682 dash_pad = '+-%s-+\n' % ('-' * tag_len) |
| 747 tag_header = '| %s |\n' % tag_header.ljust(tag_len) | 683 tag_header = '| %s |\n' % tag_header.ljust(tag_len) |
| 748 tag_footer = '| %s |\n' % tag_footer.ljust(tag_len) | 684 tag_footer = '| %s |\n' % tag_footer.ljust(tag_len) |
| 749 | 685 |
| 750 header = dash_pad + tag_header + dash_pad | 686 header = dash_pad + tag_header + dash_pad |
| 751 footer = dash_pad + tag_footer + dash_pad[:-1] | 687 footer = dash_pad + tag_footer + dash_pad[:-1] |
| 752 output = '\n'.join(o for o in metadata['outputs'] if o).rstrip() + '\n' | 688 output = metadata['output'].rstrip() + '\n' |
| 753 return header + output + footer | 689 return header + output + footer |
| 754 | 690 |
| 755 | 691 |
| 756 def collect( | 692 def collect( |
| 757 swarming, task_name, task_ids, timeout, decorate, print_status_updates, | 693 swarming, task_name, task_ids, timeout, decorate, print_status_updates, |
| 758 task_summary_json, task_output_dir): | 694 task_summary_json, task_output_dir): |
| 759 """Retrieves results of a Swarming task.""" | 695 """Retrieves results of a Swarming task.""" |
| 760 # Collect summary JSON and output files (if task_output_dir is not None). | 696 # Collect summary JSON and output files (if task_output_dir is not None). |
| 761 output_collector = TaskOutputCollector( | 697 output_collector = TaskOutputCollector( |
| 762 task_output_dir, task_name, len(task_ids)) | 698 task_output_dir, task_name, len(task_ids)) |
| 763 | 699 |
| 764 seen_shards = set() | 700 seen_shards = set() |
| 765 exit_code = 0 | 701 exit_code = 0 |
| 766 total_duration = 0 | 702 total_duration = 0 |
| 767 try: | 703 try: |
| 768 for index, metadata in yield_results( | 704 for index, metadata in yield_results( |
| 769 swarming, task_ids, timeout, None, print_status_updates, | 705 swarming, task_ids, timeout, None, print_status_updates, |
| 770 output_collector): | 706 output_collector): |
| 771 seen_shards.add(index) | 707 seen_shards.add(index) |
| 772 | 708 |
| 773 # Default to failure if there was no process that even started. | 709 # Default to failure if there was no process that even started. |
| 774 shard_exit_code = 1 | 710 shard_exit_code = metadata.get('exit_code') |
| 775 if metadata.get('exit_codes'): | 711 if shard_exit_code: |
| 776 shard_exit_code = metadata['exit_codes'][0] | 712 shard_exit_code = int(shard_exit_code) |
| 777 if shard_exit_code: | 713 if shard_exit_code: |
| 778 exit_code = shard_exit_code | 714 exit_code = shard_exit_code |
| 779 if metadata.get('durations'): | 715 total_duration += metadata.get('duration', 0) |
| 780 total_duration += metadata['durations'][0] | |
| 781 | 716 |
| 782 if decorate: | 717 if decorate: |
| 783 print(decorate_shard_output(swarming, index, metadata)) | 718 print(decorate_shard_output(swarming, index, metadata)) |
| 784 if len(seen_shards) < len(task_ids): | 719 if len(seen_shards) < len(task_ids): |
| 785 print('') | 720 print('') |
| 786 else: | 721 else: |
| 787 if metadata.get('exit_codes'): | 722 print('%s: %s %s' % ( |
| 788 exit_code = metadata['exit_codes'][0] | 723 metadata.get('bot_id', 'N/A'), |
| 789 else: | 724 metadata['task_id'], |
| 790 exit_code = 'N/A' | 725 shard_exit_code)) |
| 791 print('%s: %s %s' % | 726 if metadata['output']: |
| 792 (metadata.get('bot_id') or 'N/A', metadata['id'], exit_code)) | 727 output = metadata['output'].rstrip() |
| 793 for output in metadata['outputs']: | |
| 794 if not output: | |
| 795 continue | |
| 796 output = output.rstrip() | |
| 797 if output: | 728 if output: |
| 798 print(''.join(' %s\n' % l for l in output.splitlines())) | 729 print(''.join(' %s\n' % l for l in output.splitlines())) |
| 799 finally: | 730 finally: |
| 800 summary = output_collector.finalize() | 731 summary = output_collector.finalize() |
| 801 if task_summary_json: | 732 if task_summary_json: |
| 733 # TODO(maruel): Make this optional. | |
| 734 for i in summary['shards']: | |
| 735 if i: | |
| 736 convert_to_old_format(i) | |
| 802 tools.write_json(task_summary_json, summary, False) | 737 tools.write_json(task_summary_json, summary, False) |
| 803 | 738 |
| 804 if decorate and total_duration: | 739 if decorate and total_duration: |
| 805 print('Total duration: %.1fs' % total_duration) | 740 print('Total duration: %.1fs' % total_duration) |
| 806 | 741 |
| 807 if len(seen_shards) != len(task_ids): | 742 if len(seen_shards) != len(task_ids): |
| 808 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards] | 743 missing_shards = [x for x in range(len(task_ids)) if x not in seen_shards] |
| 809 print >> sys.stderr, ('Results from some shards are missing: %s' % | 744 print >> sys.stderr, ('Results from some shards are missing: %s' % |
| 810 ', '.join(map(str, missing_shards))) | 745 ', '.join(map(str, missing_shards))) |
| 811 return 1 | 746 return 1 |
| 812 | 747 |
| 813 return exit_code | 748 return exit_code |
| 814 | 749 |
| 815 | 750 |
| 751 ### API management. | |
| 752 | |
| 753 | |
| 754 class APIError(Exception): | |
| 755 pass | |
| 756 | |
| 757 | |
| 758 def endpoints_api_discovery_apis(host): | |
| 759 """Uses Cloud Endpoints' API Discovery Service to returns metadata about all | |
| 760 the APIs exposed by a host. | |
| 761 | |
| 762 https://developers.google.com/discovery/v1/reference/apis/list | |
| 763 """ | |
| 764 data = net.url_read_json(host + '/_ah/api/discovery/v1/apis') | |
| 765 if data is None: | |
| 766 raise APIError('Failed to discover APIs on %s' % host) | |
| 767 out = {} | |
| 768 for api in data['items']: | |
| 769 if api['id'] == 'discovery:v1': | |
| 770 continue | |
| 771 # URL is of the following form: | |
| 772 # url = host + ( | |
| 773 # '/_ah/api/discovery/v1/apis/%s/%s/rest' % (api['id'], api['version']) | |
| 774 api_data = net.url_read_json(api['discoveryRestUrl']) | |
| 775 if api_data is None: | |
| 776 raise APIError('Failed to discover %s on %s' % (api['id'], host)) | |
| 777 out[api['id']] = api_data | |
| 778 return out | |
| 779 | |
| 780 | |
| 816 ### Commands. | 781 ### Commands. |
| 817 | 782 |
| 818 | 783 |
| 819 def abort_task(_swarming, _manifest): | 784 def abort_task(_swarming, _manifest): |
| 820 """Given a task manifest that was triggered, aborts its execution.""" | 785 """Given a task manifest that was triggered, aborts its execution.""" |
| 821 # TODO(vadimsh): No supported by the server yet. | 786 # TODO(vadimsh): No supported by the server yet. |
| 822 | 787 |
| 823 | 788 |
| 824 def add_filter_options(parser): | 789 def add_filter_options(parser): |
| 825 parser.filter_group = optparse.OptionGroup(parser, 'Filtering slaves') | 790 parser.filter_group = optparse.OptionGroup(parser, 'Filtering slaves') |
| (...skipping 63 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 889 'In this case, no .isolated file is expected.') | 854 'In this case, no .isolated file is expected.') |
| 890 parser.add_option_group(parser.task_group) | 855 parser.add_option_group(parser.task_group) |
| 891 | 856 |
| 892 | 857 |
| 893 def process_trigger_options(parser, options, args): | 858 def process_trigger_options(parser, options, args): |
| 894 """Processes trigger options and uploads files to isolate server if necessary. | 859 """Processes trigger options and uploads files to isolate server if necessary. |
| 895 """ | 860 """ |
| 896 options.dimensions = dict(options.dimensions) | 861 options.dimensions = dict(options.dimensions) |
| 897 options.env = dict(options.env) | 862 options.env = dict(options.env) |
| 898 | 863 |
| 899 data = [] | |
| 900 if not options.dimensions: | 864 if not options.dimensions: |
| 901 parser.error('Please at least specify one --dimension') | 865 parser.error('Please at least specify one --dimension') |
| 902 if options.raw_cmd: | 866 if options.raw_cmd: |
| 903 if not args: | 867 if not args: |
| 904 parser.error( | 868 parser.error( |
| 905 'Arguments with --raw-cmd should be passed after -- as command ' | 869 'Arguments with --raw-cmd should be passed after -- as command ' |
| 906 'delimiter.') | 870 'delimiter.') |
| 907 if options.isolate_server: | 871 if options.isolate_server: |
| 908 parser.error('Can\'t use both --raw-cmd and --isolate-server.') | 872 parser.error('Can\'t use both --raw-cmd and --isolate-server.') |
| 909 | 873 |
| 910 command = args | 874 command = args |
| 911 if not options.task_name: | 875 if not options.task_name: |
| 912 options.task_name = u'%s/%s' % ( | 876 options.task_name = u'%s/%s' % ( |
| 913 options.user, | 877 options.user, |
| 914 '_'.join( | 878 '_'.join( |
| 915 '%s=%s' % (k, v) | 879 '%s=%s' % (k, v) |
| 916 for k, v in sorted(options.dimensions.iteritems()))) | 880 for k, v in sorted(options.dimensions.iteritems()))) |
| 881 inputs_ref = None | |
| 917 else: | 882 else: |
| 918 isolateserver.process_isolate_server_options(parser, options, False) | 883 isolateserver.process_isolate_server_options(parser, options, False) |
| 919 try: | 884 try: |
| 920 command, data = isolated_handle_options(options, args) | 885 command, inputs_ref = isolated_handle_options(options, args) |
| 921 except ValueError as e: | 886 except ValueError as e: |
| 922 parser.error(str(e)) | 887 parser.error(str(e)) |
| 923 | 888 |
| 924 return TaskRequest( | 889 # If inputs_ref is used, command is actually extra_args. Otherwise it's an |
| 925 command=command, | 890 # actual command to run. |
| 926 data=data, | 891 properties = TaskProperties( |
| 892 command=None if inputs_ref else command, | |
| 927 dimensions=options.dimensions, | 893 dimensions=options.dimensions, |
| 928 env=options.env, | 894 env=options.env, |
| 929 expiration=options.expiration, | 895 execution_timeout_secs=options.hard_timeout, |
| 930 hard_timeout=options.hard_timeout, | 896 extra_args=command if inputs_ref else None, |
| 897 grace_period_secs=30, | |
| 931 idempotent=options.idempotent, | 898 idempotent=options.idempotent, |
| 932 io_timeout=options.io_timeout, | 899 inputs_ref=inputs_ref, |
| 900 io_timeout_secs=options.io_timeout) | |
| 901 return NewTaskRequest( | |
| 902 expiration_secs=options.expiration, | |
| 933 name=options.task_name, | 903 name=options.task_name, |
| 904 parent_task_id=os.environ.get('SWARMING_TASK_ID', ''), | |
| 934 priority=options.priority, | 905 priority=options.priority, |
| 906 properties=properties, | |
| 935 tags=options.tags, | 907 tags=options.tags, |
| 936 user=options.user, | 908 user=options.user) |
| 937 verbose=options.verbose) | |
| 938 | 909 |
| 939 | 910 |
| 940 def add_collect_options(parser): | 911 def add_collect_options(parser): |
| 941 parser.server_group.add_option( | 912 parser.server_group.add_option( |
| 942 '-t', '--timeout', | 913 '-t', '--timeout', |
| 943 type='float', | 914 type='float', |
| 944 default=80*60., | 915 default=80*60., |
| 945 help='Timeout to wait for result, set to 0 for no timeout; default: ' | 916 help='Timeout to wait for result, set to 0 for no timeout; default: ' |
| 946 '%default s') | 917 '%default s') |
| 947 parser.group_logging.add_option( | 918 parser.group_logging.add_option( |
| (...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 979 if not options.force: | 950 if not options.force: |
| 980 print('Delete the following bots?') | 951 print('Delete the following bots?') |
| 981 for bot in bots: | 952 for bot in bots: |
| 982 print(' %s' % bot) | 953 print(' %s' % bot) |
| 983 if raw_input('Continue? [y/N] ') not in ('y', 'Y'): | 954 if raw_input('Continue? [y/N] ') not in ('y', 'Y'): |
| 984 print('Goodbye.') | 955 print('Goodbye.') |
| 985 return 1 | 956 return 1 |
| 986 | 957 |
| 987 result = 0 | 958 result = 0 |
| 988 for bot in bots: | 959 for bot in bots: |
| 989 url = '%s/swarming/api/v1/client/bot/%s' % (options.swarming, bot) | 960 url = '%s/_ah/api/swarming/v1/bot/%s' % (options.swarming, bot) |
| 990 if net.url_read_json(url, method='DELETE') is None: | 961 if net.url_read_json(url, method='DELETE') is None: |
| 991 print('Deleting %s failed' % bot) | 962 print('Deleting %s failed' % bot) |
| 992 result = 1 | 963 result = 1 |
| 993 return result | 964 return result |
| 994 | 965 |
| 995 | 966 |
| 996 def CMDbots(parser, args): | 967 def CMDbots(parser, args): |
| 997 """Returns information about the bots connected to the Swarming server.""" | 968 """Returns information about the bots connected to the Swarming server.""" |
| 998 add_filter_options(parser) | 969 add_filter_options(parser) |
| 999 parser.filter_group.add_option( | 970 parser.filter_group.add_option( |
| 1000 '--dead-only', action='store_true', | 971 '--dead-only', action='store_true', |
| 1001 help='Only print dead bots, useful to reap them and reimage broken bots') | 972 help='Only print dead bots, useful to reap them and reimage broken bots') |
| 1002 parser.filter_group.add_option( | 973 parser.filter_group.add_option( |
| 1003 '-k', '--keep-dead', action='store_true', | 974 '-k', '--keep-dead', action='store_true', |
| 1004 help='Do not filter out dead bots') | 975 help='Do not filter out dead bots') |
| 1005 parser.filter_group.add_option( | 976 parser.filter_group.add_option( |
| 1006 '-b', '--bare', action='store_true', | 977 '-b', '--bare', action='store_true', |
| 1007 help='Do not print out dimensions') | 978 help='Do not print out dimensions') |
| 1008 options, args = parser.parse_args(args) | 979 options, args = parser.parse_args(args) |
| 1009 | 980 |
| 1010 if options.keep_dead and options.dead_only: | 981 if options.keep_dead and options.dead_only: |
| 1011 parser.error('Use only one of --keep-dead and --dead-only') | 982 parser.error('Use only one of --keep-dead and --dead-only') |
| 1012 | 983 |
| 1013 bots = [] | 984 bots = [] |
| 1014 cursor = None | 985 cursor = None |
| 1015 limit = 250 | 986 limit = 250 |
| 1016 # Iterate via cursors. | 987 # Iterate via cursors. |
| 1017 base_url = options.swarming + '/swarming/api/v1/client/bots?limit=%d' % limit | 988 base_url = ( |
| 989 options.swarming + '/_ah/api/swarming/v1/bots/list?limit=%d' % limit) | |
| 1018 while True: | 990 while True: |
| 1019 url = base_url | 991 url = base_url |
| 1020 if cursor: | 992 if cursor: |
| 1021 url += '&cursor=%s' % urllib.quote(cursor) | 993 url += '&cursor=%s' % urllib.quote(cursor) |
| 1022 data = net.url_read_json(url) | 994 data = net.url_read_json(url) |
| 1023 if data is None: | 995 if data is None: |
| 1024 print >> sys.stderr, 'Failed to access %s' % options.swarming | 996 print >> sys.stderr, 'Failed to access %s' % options.swarming |
| 1025 return 1 | 997 return 1 |
| 1026 bots.extend(data['items']) | 998 bots.extend(data['items']) |
| 1027 cursor = data['cursor'] | 999 cursor = data.get('cursor') |
| 1028 if not cursor: | 1000 if not cursor: |
| 1029 break | 1001 break |
| 1030 | 1002 |
| 1031 for bot in natsort.natsorted(bots, key=lambda x: x['id']): | 1003 for bot in natsort.natsorted(bots, key=lambda x: x['bot_id']): |
| 1032 if options.dead_only: | 1004 if options.dead_only: |
| 1033 if not bot['is_dead']: | 1005 if not bot.get('is_dead'): |
| 1034 continue | 1006 continue |
| 1035 elif not options.keep_dead and bot['is_dead']: | 1007 elif not options.keep_dead and bot.get('is_dead'): |
| 1036 continue | 1008 continue |
| 1037 | 1009 |
| 1038 # If the user requested to filter on dimensions, ensure the bot has all the | 1010 # If the user requested to filter on dimensions, ensure the bot has all the |
| 1039 # dimensions requested. | 1011 # dimensions requested. |
| 1040 dimensions = bot['dimensions'] | 1012 dimensions = {i['key']: i['value'] for i in bot['dimensions']} |
| 1041 for key, value in options.dimensions: | 1013 for key, value in options.dimensions: |
| 1042 if key not in dimensions: | 1014 if key not in dimensions: |
| 1043 break | 1015 break |
| 1044 # A bot can have multiple value for a key, for example, | 1016 # A bot can have multiple value for a key, for example, |
| 1045 # {'os': ['Windows', 'Windows-6.1']}, so that --dimension os=Windows will | 1017 # {'os': ['Windows', 'Windows-6.1']}, so that --dimension os=Windows will |
| 1046 # be accepted. | 1018 # be accepted. |
| 1047 if isinstance(dimensions[key], list): | 1019 if isinstance(dimensions[key], list): |
| 1048 if value not in dimensions[key]: | 1020 if value not in dimensions[key]: |
| 1049 break | 1021 break |
| 1050 else: | 1022 else: |
| 1051 if value != dimensions[key]: | 1023 if value != dimensions[key]: |
| 1052 break | 1024 break |
| 1053 else: | 1025 else: |
| 1054 print bot['id'] | 1026 print bot['bot_id'] |
| 1055 if not options.bare: | 1027 if not options.bare: |
| 1056 print ' %s' % json.dumps(dimensions, sort_keys=True) | 1028 print ' %s' % json.dumps(dimensions, sort_keys=True) |
| 1057 if bot.get('task_id'): | 1029 if bot.get('task_id'): |
| 1058 print ' task: %s' % bot['task_id'] | 1030 print ' task: %s' % bot['task_id'] |
| 1059 return 0 | 1031 return 0 |
| 1060 | 1032 |
| 1061 | 1033 |
| 1062 @subcommand.usage('--json file | task_id...') | 1034 @subcommand.usage('--json file | task_id...') |
| 1063 def CMDcollect(parser, args): | 1035 def CMDcollect(parser, args): |
| 1064 """Retrieves results of one or multiple Swarming task by its ID. | 1036 """Retrieves results of one or multiple Swarming task by its ID. |
| 1065 | 1037 |
| 1066 The result can be in multiple part if the execution was sharded. It can | 1038 The result can be in multiple part if the execution was sharded. It can |
| 1067 potentially have retries. | 1039 potentially have retries. |
| 1068 """ | 1040 """ |
| 1069 add_collect_options(parser) | 1041 add_collect_options(parser) |
| 1070 parser.add_option( | 1042 parser.add_option( |
| 1071 '-j', '--json', | 1043 '-j', '--json', |
| 1072 help='Load the task ids from .json as saved by trigger --dump-json') | 1044 help='Load the task ids from .json as saved by trigger --dump-json') |
| 1073 (options, args) = parser.parse_args(args) | 1045 options, args = parser.parse_args(args) |
| 1074 if not args and not options.json: | 1046 if not args and not options.json: |
| 1075 parser.error('Must specify at least one task id or --json.') | 1047 parser.error('Must specify at least one task id or --json.') |
| 1076 if args and options.json: | 1048 if args and options.json: |
| 1077 parser.error('Only use one of task id or --json.') | 1049 parser.error('Only use one of task id or --json.') |
| 1078 | 1050 |
| 1079 if options.json: | 1051 if options.json: |
| 1080 try: | 1052 try: |
| 1081 with open(options.json) as f: | 1053 with open(options.json) as f: |
| 1082 tasks = sorted( | 1054 tasks = sorted( |
| 1083 json.load(f)['tasks'].itervalues(), key=lambda x: x['shard_index']) | 1055 json.load(f)['tasks'].itervalues(), key=lambda x: x['shard_index']) |
| (...skipping 13 matching lines...) Expand all Loading... | |
| 1097 options.timeout, | 1069 options.timeout, |
| 1098 options.decorate, | 1070 options.decorate, |
| 1099 options.print_status_updates, | 1071 options.print_status_updates, |
| 1100 options.task_summary_json, | 1072 options.task_summary_json, |
| 1101 options.task_output_dir) | 1073 options.task_output_dir) |
| 1102 except Failure: | 1074 except Failure: |
| 1103 on_error.report(None) | 1075 on_error.report(None) |
| 1104 return 1 | 1076 return 1 |
| 1105 | 1077 |
| 1106 | 1078 |
| 1107 @subcommand.usage('[resource name]') | 1079 @subcommand.usage('[method name]') |
| 1108 def CMDquery(parser, args): | 1080 def CMDquery(parser, args): |
| 1109 """Returns raw JSON information via an URL endpoint. Use 'list' to gather the | 1081 """Returns raw JSON information via an URL endpoint. Use 'query-list' to |
| 1110 list of valid values from the server. | 1082 gather the list of API methods from the server. |
| 1111 | 1083 |
| 1112 Examples: | 1084 Examples: |
| 1113 Printing the list of known URLs: | 1085 Listing all bots: |
| 1114 swarming.py query -S https://server-url list | 1086 swarming.py query -S https://server-url bots/list |
| 1115 | 1087 |
| 1116 Listing last 50 tasks on a specific bot named 'swarm1' | 1088 Listing last 10 tasks on a specific bot named 'swarm1': |
| 1117 swarming.py query -S https://server-url --limit 50 bot/swarm1/tasks | 1089 swarming.py query -S https://server-url --limit 10 bot/swarm1/tasks |
| 1118 """ | 1090 """ |
| 1119 CHUNK_SIZE = 250 | 1091 CHUNK_SIZE = 250 |
| 1120 | 1092 |
| 1121 parser.add_option( | 1093 parser.add_option( |
| 1122 '-L', '--limit', type='int', default=200, | 1094 '-L', '--limit', type='int', default=200, |
| 1123 help='Limit to enforce on limitless items (like number of tasks); ' | 1095 help='Limit to enforce on limitless items (like number of tasks); ' |
| 1124 'default=%default') | 1096 'default=%default') |
| 1125 parser.add_option( | 1097 parser.add_option( |
| 1126 '--json', help='Path to JSON output file (otherwise prints to stdout)') | 1098 '--json', help='Path to JSON output file (otherwise prints to stdout)') |
| 1127 (options, args) = parser.parse_args(args) | 1099 parser.add_option( |
| 1100 '--progress', action='store_true', | |
| 1101 help='Prints a dot at each request to show progress') | |
| 1102 options, args = parser.parse_args(args) | |
| 1128 if len(args) != 1: | 1103 if len(args) != 1: |
| 1129 parser.error('Must specify only one resource name.') | 1104 parser.error( |
| 1130 | 1105 'Must specify only method name and optionally query args properly ' |
| 1131 base_url = options.swarming + '/swarming/api/v1/client/' + args[0] | 1106 'escaped.') |
| 1107 base_url = options.swarming + '/_ah/api/swarming/v1/' + args[0] | |
| 1132 url = base_url | 1108 url = base_url |
| 1133 if options.limit: | 1109 if options.limit: |
| 1134 # Check check, change if not working out. | 1110 # Check check, change if not working out. |
| 1135 merge_char = '&' if '?' in url else '?' | 1111 merge_char = '&' if '?' in url else '?' |
| 1136 url += '%slimit=%d' % (merge_char, min(CHUNK_SIZE, options.limit)) | 1112 url += '%slimit=%d' % (merge_char, min(CHUNK_SIZE, options.limit)) |
| 1137 data = net.url_read_json(url) | 1113 data = net.url_read_json(url) |
| 1138 if data is None: | 1114 if data is None: |
| 1139 print >> sys.stderr, 'Failed to access %s' % options.swarming | 1115 # TODO(maruel): Do basic diagnostic. |
| 1116 print >> sys.stderr, 'Failed to access %s' % url | |
| 1140 return 1 | 1117 return 1 |
| 1141 | 1118 |
| 1142 # Some items support cursors. Try to get automatically if cursors are needed | 1119 # Some items support cursors. Try to get automatically if cursors are needed |
| 1143 # by looking at the 'cursor' items. | 1120 # by looking at the 'cursor' items. |
| 1144 while ( | 1121 while ( |
| 1145 data.get('cursor') and | 1122 data.get('cursor') and |
| 1146 (not options.limit or len(data['items']) < options.limit)): | 1123 (not options.limit or len(data['items']) < options.limit)): |
| 1147 merge_char = '&' if '?' in base_url else '?' | 1124 merge_char = '&' if '?' in base_url else '?' |
| 1148 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(data['cursor'])) | 1125 url = base_url + '%scursor=%s' % (merge_char, urllib.quote(data['cursor'])) |
| 1149 if options.limit: | 1126 if options.limit: |
| 1150 url += '&limit=%d' % min(CHUNK_SIZE, options.limit - len(data['items'])) | 1127 url += '&limit=%d' % min(CHUNK_SIZE, options.limit - len(data['items'])) |
| 1128 if options.progress: | |
| 1129 sys.stdout.write('.') | |
| 1130 sys.stdout.flush() | |
| 1151 new = net.url_read_json(url) | 1131 new = net.url_read_json(url) |
| 1152 if new is None: | 1132 if new is None: |
| 1133 if options.progress: | |
| 1134 print('') | |
| 1153 print >> sys.stderr, 'Failed to access %s' % options.swarming | 1135 print >> sys.stderr, 'Failed to access %s' % options.swarming |
| 1154 return 1 | 1136 return 1 |
| 1155 data['items'].extend(new['items']) | 1137 data['items'].extend(new['items']) |
| 1156 data['cursor'] = new['cursor'] | 1138 data['cursor'] = new.get('cursor') |
| 1157 | 1139 |
| 1140 if options.progress: | |
| 1141 print('') | |
| 1158 if options.limit and len(data.get('items', [])) > options.limit: | 1142 if options.limit and len(data.get('items', [])) > options.limit: |
| 1159 data['items'] = data['items'][:options.limit] | 1143 data['items'] = data['items'][:options.limit] |
| 1160 data.pop('cursor', None) | 1144 data.pop('cursor', None) |
| 1161 | 1145 |
| 1162 if options.json: | 1146 if options.json: |
| 1163 with open(options.json, 'w') as f: | 1147 tools.write_json(options.json, data, True) |
| 1164 json.dump(data, f) | |
| 1165 else: | 1148 else: |
| 1166 try: | 1149 try: |
| 1167 json.dump(data, sys.stdout, indent=2, sort_keys=True) | 1150 tools.write_json(sys.stdout, data, False) |
| 1168 sys.stdout.write('\n') | 1151 sys.stdout.write('\n') |
| 1169 except IOError: | 1152 except IOError: |
| 1170 pass | 1153 pass |
| 1171 return 0 | 1154 return 0 |
| 1172 | 1155 |
| 1173 | 1156 |
| 1157 def CMDquery_list(parser, args): | |
| 1158 """Returns list of all the Swarming APIs that can be used with command | |
| 1159 'query'. | |
| 1160 """ | |
| 1161 parser.add_option( | |
| 1162 '--json', help='Path to JSON output file (otherwise prints to stdout)') | |
| 1163 options, args = parser.parse_args(args) | |
| 1164 if args: | |
| 1165 parser.error('No argument allowed.') | |
| 1166 | |
| 1167 try: | |
| 1168 apis = endpoints_api_discovery_apis(options.swarming) | |
| 1169 except APIError as e: | |
| 1170 parser.error(str(e)) | |
| 1171 if options.json: | |
| 1172 with open(options.json, 'wb') as f: | |
| 1173 json.dump(apis, f) | |
| 1174 else: | |
| 1175 help_url = ( | |
| 1176 'https://apis-explorer.appspot.com/apis-explorer/?base=%s/_ah/api#p/' % | |
| 1177 options.swarming) | |
| 1178 for api_id, api in sorted(apis.iteritems()): | |
| 1179 print api_id | |
| 1180 print ' ' + api['description'] | |
| 1181 for resource_name, resource in sorted(api['resources'].iteritems()): | |
| 1182 print '' | |
| 1183 for method_name, method in sorted(resource['methods'].iteritems()): | |
| 1184 # Only list the GET ones. | |
| 1185 if method['httpMethod'] != 'GET': | |
| 1186 continue | |
| 1187 print '- %s.%s: %s' % ( | |
| 1188 resource_name, method_name, method['path']) | |
| 1189 print ' ' + method['description'] | |
| 1190 print ' %s%s%s' % (help_url, api['servicePath'], method['id']) | |
| 1191 return 0 | |
| 1192 | |
| 1193 | |
| 1174 @subcommand.usage('(hash|isolated) [-- extra_args]') | 1194 @subcommand.usage('(hash|isolated) [-- extra_args]') |
| 1175 def CMDrun(parser, args): | 1195 def CMDrun(parser, args): |
| 1176 """Triggers a task and wait for the results. | 1196 """Triggers a task and wait for the results. |
| 1177 | 1197 |
| 1178 Basically, does everything to run a command remotely. | 1198 Basically, does everything to run a command remotely. |
| 1179 """ | 1199 """ |
| 1180 add_trigger_options(parser) | 1200 add_trigger_options(parser) |
| 1181 add_collect_options(parser) | 1201 add_collect_options(parser) |
| 1182 add_sharding_options(parser) | 1202 add_sharding_options(parser) |
| 1183 options, args = parser.parse_args(args) | 1203 options, args = parser.parse_args(args) |
| (...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 1218 """Runs a task locally that was triggered on the server. | 1238 """Runs a task locally that was triggered on the server. |
| 1219 | 1239 |
| 1220 This running locally the same commands that have been run on the bot. The data | 1240 This running locally the same commands that have been run on the bot. The data |
| 1221 downloaded will be in a subdirectory named 'work' of the current working | 1241 downloaded will be in a subdirectory named 'work' of the current working |
| 1222 directory. | 1242 directory. |
| 1223 """ | 1243 """ |
| 1224 options, args = parser.parse_args(args) | 1244 options, args = parser.parse_args(args) |
| 1225 if len(args) != 1: | 1245 if len(args) != 1: |
| 1226 parser.error('Must specify exactly one task id.') | 1246 parser.error('Must specify exactly one task id.') |
| 1227 | 1247 |
| 1228 url = options.swarming + '/swarming/api/v1/client/task/%s/request' % args[0] | 1248 url = options.swarming + '/_ah/api/swarming/v1/task/%s/request' % args[0] |
| 1229 request = net.url_read_json(url) | 1249 request = net.url_read_json(url) |
| 1230 if not request: | 1250 if not request: |
| 1231 print >> sys.stderr, 'Failed to retrieve request data for the task' | 1251 print >> sys.stderr, 'Failed to retrieve request data for the task' |
| 1232 return 1 | 1252 return 1 |
| 1233 | 1253 |
| 1234 if not os.path.isdir('work'): | 1254 if not os.path.isdir('work'): |
| 1235 os.mkdir('work') | 1255 os.mkdir('work') |
| 1236 | 1256 |
| 1237 swarming_host = urlparse.urlparse(options.swarming).netloc | |
| 1238 properties = request['properties'] | 1257 properties = request['properties'] |
| 1239 for data_url, _ in properties['data']: | |
| 1240 assert data_url.startswith('https://'), data_url | |
| 1241 data_host = urlparse.urlparse(data_url).netloc | |
| 1242 if data_host != swarming_host: | |
| 1243 auth.ensure_logged_in('https://' + data_host) | |
| 1244 | |
| 1245 content = net.url_read(data_url) | |
| 1246 if content is None: | |
| 1247 print >> sys.stderr, 'Failed to download %s' % data_url | |
| 1248 return 1 | |
| 1249 with zipfile.ZipFile(StringIO.StringIO(content)) as zip_file: | |
| 1250 zip_file.extractall('work') | |
| 1251 | |
| 1252 env = None | 1258 env = None |
| 1253 if properties['env']: | 1259 if properties['env']: |
| 1254 env = os.environ.copy() | 1260 env = os.environ.copy() |
| 1255 logging.info('env: %r', properties['env']) | 1261 logging.info('env: %r', properties['env']) |
| 1256 env.update( | 1262 env.update( |
| 1257 (k.encode('utf-8'), v.encode('utf-8')) | 1263 (i['key'].encode('utf-8'), i['value'].encode('utf-8')) |
| 1258 for k, v in properties['env'].iteritems()) | 1264 for i in properties['env']) |
| 1259 | 1265 |
| 1260 exit_code = 0 | 1266 try: |
| 1261 for cmd in properties['commands']: | 1267 return subprocess.call(properties['command'], env=env, cwd='work') |
| 1262 try: | 1268 except OSError as e: |
| 1263 c = subprocess.call(cmd, env=env, cwd='work') | 1269 print >> sys.stderr, 'Failed to run: %s' % ' '.join(properties['command']) |
| 1264 except OSError as e: | 1270 print >> sys.stderr, str(e) |
| 1265 print >> sys.stderr, 'Failed to run: %s' % ' '.join(cmd) | 1271 return 1 |
| 1266 print >> sys.stderr, str(e) | |
| 1267 c = 1 | |
| 1268 if not exit_code: | |
| 1269 exit_code = c | |
| 1270 return exit_code | |
| 1271 | 1272 |
| 1272 | 1273 |
| 1273 @subcommand.usage("(hash|isolated) [-- extra_args|raw command]") | 1274 @subcommand.usage("(hash|isolated) [-- extra_args|raw command]") |
| 1274 def CMDtrigger(parser, args): | 1275 def CMDtrigger(parser, args): |
| 1275 """Triggers a Swarming task. | 1276 """Triggers a Swarming task. |
| 1276 | 1277 |
| 1277 Accepts either the hash (sha1) of a .isolated file already uploaded or the | 1278 Accepts either the hash (sha1) of a .isolated file already uploaded or the |
| 1278 path to an .isolated file to archive. | 1279 path to an .isolated file to archive. |
| 1279 | 1280 |
| 1280 If an .isolated file is specified instead of an hash, it is first archived. | 1281 If an .isolated file is specified instead of an hash, it is first archived. |
| (...skipping 81 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 1362 def main(args): | 1363 def main(args): |
| 1363 dispatcher = subcommand.CommandDispatcher(__name__) | 1364 dispatcher = subcommand.CommandDispatcher(__name__) |
| 1364 return dispatcher.execute(OptionParserSwarming(version=__version__), args) | 1365 return dispatcher.execute(OptionParserSwarming(version=__version__), args) |
| 1365 | 1366 |
| 1366 | 1367 |
| 1367 if __name__ == '__main__': | 1368 if __name__ == '__main__': |
| 1368 fix_encoding.fix_encoding() | 1369 fix_encoding.fix_encoding() |
| 1369 tools.disable_buffering() | 1370 tools.disable_buffering() |
| 1370 colorama.init() | 1371 colorama.init() |
| 1371 sys.exit(main(sys.argv[1:])) | 1372 sys.exit(main(sys.argv[1:])) |
| OLD | NEW |