Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1175)

Side by Side Diff: client/run_isolated.py

Issue 2037253002: run_isolated.py: install CIPD packages (Closed) Base URL: https://chromium.googlesource.com/external/github.com/luci/luci-py@master
Patch Set: simplified closing of list file Created 4 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 #!/usr/bin/env python 1 #!/usr/bin/env python
2 # Copyright 2012 The LUCI Authors. All rights reserved. 2 # Copyright 2012 The LUCI Authors. All rights reserved.
3 # Use of this source code is governed under the Apache License, Version 2.0 3 # Use of this source code is governed under the Apache License, Version 2.0
4 # that can be found in the LICENSE file. 4 # that can be found in the LICENSE file.
5 5
6 """Runs a command with optional isolated input/output. 6 """Runs a command with optional isolated input/output.
7 7
8 Despite name "run_isolated", can run a generic non-isolated command specified as 8 Despite name "run_isolated", can run a generic non-isolated command specified as
9 args. 9 args.
10 10
11 If input isolated hash is provided, fetches it, creates a tree of hard links, 11 If input isolated hash is provided, fetches it, creates a tree of hard links,
12 appends args to the command in the fetched isolated and runs it. 12 appends args to the command in the fetched isolated and runs it.
13 To improve performance, keeps a local cache. 13 To improve performance, keeps a local cache.
14 The local cache can safely be deleted. 14 The local cache can safely be deleted.
15 15
16 Any ${EXECUTABLE_SUFFIX} on the command line will be replaced with ".exe" string
17 on Windows and "" on other platforms.
18
19 If at least one CIPD package was specified, any ${CIPD_PATH} on the command line
20 will be replaced by location of a temporary directory that contains installed
21 packages.
22
16 Any ${ISOLATED_OUTDIR} on the command line will be replaced by the location of a 23 Any ${ISOLATED_OUTDIR} on the command line will be replaced by the location of a
17 temporary directory upon execution of the command specified in the .isolated 24 temporary directory upon execution of the command specified in the .isolated
18 file. All content written to this directory will be uploaded upon termination 25 file. All content written to this directory will be uploaded upon termination
19 and the .isolated file describing this directory will be printed to stdout. 26 and the .isolated file describing this directory will be printed to stdout.
20 """ 27 """
21 28
22 __version__ = '0.7.0' 29 __version__ = '0.8.0'
23 30
24 import base64 31 import base64
32 import contextlib
33 import hashlib
25 import logging 34 import logging
26 import optparse 35 import optparse
27 import os 36 import os
28 import sys 37 import sys
29 import tempfile 38 import tempfile
30 import time 39 import time
31 40
32 from third_party.depot_tools import fix_encoding 41 from third_party.depot_tools import fix_encoding
33 42
34 from utils import file_path 43 from utils import file_path
35 from utils import fs 44 from utils import fs
36 from utils import large 45 from utils import large
37 from utils import logging_utils 46 from utils import logging_utils
38 from utils import on_error 47 from utils import on_error
39 from utils import subprocess42 48 from utils import subprocess42
40 from utils import tools 49 from utils import tools
41 from utils import zip_package 50 from utils import zip_package
42 51
43 import auth 52 import auth
53 import cipd
44 import isolateserver 54 import isolateserver
45 55
46 56
47 ISOLATED_OUTDIR_PARAMETER = '${ISOLATED_OUTDIR}' 57 ISOLATED_OUTDIR_PARAMETER = '${ISOLATED_OUTDIR}'
58 CIPD_PATH_PARAMETER = '${CIPD_PATH}'
59 EXECUTABLE_SUFFIX_PARAMETER = '${EXECUTABLE_SUFFIX}'
48 60
49 # Absolute path to this file (can be None if running from zip on Mac). 61 # Absolute path to this file (can be None if running from zip on Mac).
50 THIS_FILE_PATH = os.path.abspath(__file__) if __file__ else None 62 THIS_FILE_PATH = os.path.abspath(__file__) if __file__ else None
51 63
52 # Directory that contains this file (might be inside zip package). 64 # Directory that contains this file (might be inside zip package).
53 BASE_DIR = os.path.dirname(THIS_FILE_PATH) if __file__ else None 65 BASE_DIR = os.path.dirname(THIS_FILE_PATH) if __file__ else None
54 66
55 # Directory that contains currently running script file. 67 # Directory that contains currently running script file.
56 if zip_package.get_main_script_path(): 68 if zip_package.get_main_script_path():
57 MAIN_DIR = os.path.dirname( 69 MAIN_DIR = os.path.dirname(
(...skipping 19 matching lines...) Expand all
77 # Building a zip package when running from another zip package is 89 # Building a zip package when running from another zip package is
78 # unsupported and probably unneeded. 90 # unsupported and probably unneeded.
79 assert not zip_package.is_zipped_module(sys.modules[__name__]) 91 assert not zip_package.is_zipped_module(sys.modules[__name__])
80 assert THIS_FILE_PATH 92 assert THIS_FILE_PATH
81 assert BASE_DIR 93 assert BASE_DIR
82 package = zip_package.ZipPackage(root=BASE_DIR) 94 package = zip_package.ZipPackage(root=BASE_DIR)
83 package.add_python_file(THIS_FILE_PATH, '__main__.py' if executable else None) 95 package.add_python_file(THIS_FILE_PATH, '__main__.py' if executable else None)
84 package.add_python_file(os.path.join(BASE_DIR, 'isolated_format.py')) 96 package.add_python_file(os.path.join(BASE_DIR, 'isolated_format.py'))
85 package.add_python_file(os.path.join(BASE_DIR, 'isolateserver.py')) 97 package.add_python_file(os.path.join(BASE_DIR, 'isolateserver.py'))
86 package.add_python_file(os.path.join(BASE_DIR, 'auth.py')) 98 package.add_python_file(os.path.join(BASE_DIR, 'auth.py'))
99 package.add_python_file(os.path.join(BASE_DIR, 'cipd.py'))
87 package.add_directory(os.path.join(BASE_DIR, 'third_party')) 100 package.add_directory(os.path.join(BASE_DIR, 'third_party'))
88 package.add_directory(os.path.join(BASE_DIR, 'utils')) 101 package.add_directory(os.path.join(BASE_DIR, 'utils'))
89 return package 102 return package
90 103
91 104
92 def make_temp_dir(prefix, root_dir=None): 105 def make_temp_dir(prefix, root_dir=None):
93 """Returns a temporary directory. 106 """Returns a temporary directory.
94 107
95 If root_dir is given and /tmp is on same file system as root_dir, uses /tmp. 108 If root_dir is given and /tmp is on same file system as root_dir, uses /tmp.
96 Otherwise makes a new temp directory under root_dir. 109 Otherwise makes a new temp directory under root_dir.
(...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after
133 # is not yet changed to verify the hash of the content of the files it is 146 # is not yet changed to verify the hash of the content of the files it is
134 # looking at, so that if a test modifies an input file, the file must be 147 # looking at, so that if a test modifies an input file, the file must be
135 # deleted. 148 # deleted.
136 file_path.make_tree_writeable(rootdir) 149 file_path.make_tree_writeable(rootdir)
137 else: 150 else:
138 raise ValueError( 151 raise ValueError(
139 'change_tree_read_only(%s, %s): Unknown flag %s' % 152 'change_tree_read_only(%s, %s): Unknown flag %s' %
140 (rootdir, read_only, read_only)) 153 (rootdir, read_only, read_only))
141 154
142 155
143 def process_command(command, out_dir): 156 def process_command(command, out_dir, cipd_path):
144 """Replaces isolated specific variables in a command line.""" 157 """Replaces variables in a command line.
158
159 Raises:
160 ValueError if a parameter is requested in |command| but its value is not
161 provided.
162 """
145 def fix(arg): 163 def fix(arg):
164 arg = arg.replace(EXECUTABLE_SUFFIX_PARAMETER, cipd.EXECUTABLE_SUFFIX)
165 replace_slash = False
166 if CIPD_PATH_PARAMETER in arg:
167 if not cipd_path:
168 raise ValueError('cipd_path is requested in command, but not provided')
169 arg = arg.replace(CIPD_PATH_PARAMETER, cipd_path)
170 replace_slash = True
146 if ISOLATED_OUTDIR_PARAMETER in arg: 171 if ISOLATED_OUTDIR_PARAMETER in arg:
147 assert out_dir 172 if not out_dir:
173 raise ValueError('out_dir is requested in command, but not provided')
148 arg = arg.replace(ISOLATED_OUTDIR_PARAMETER, out_dir) 174 arg = arg.replace(ISOLATED_OUTDIR_PARAMETER, out_dir)
149 # Replace slashes only if ISOLATED_OUTDIR_PARAMETER is present 175 replace_slash = True
176 if replace_slash:
177 # Replace slashes only if parameters are present
150 # because of arguments like '${ISOLATED_OUTDIR}/foo/bar' 178 # because of arguments like '${ISOLATED_OUTDIR}/foo/bar'
151 arg = arg.replace('/', os.sep) 179 arg = arg.replace('/', os.sep)
152 return arg 180 return arg
153 181
154 return [fix(arg) for arg in command] 182 return [fix(arg) for arg in command]
155 183
156 184
157 def run_command(command, cwd, tmp_dir, hard_timeout, grace_period): 185 def run_command(command, cwd, tmp_dir, hard_timeout, grace_period):
158 """Runs the command. 186 """Runs the command.
159 187
(...skipping 146 matching lines...) Expand 10 before | Expand all | Expand 10 after
306 stats = { 334 stats = {
307 'duration': time.time() - start, 335 'duration': time.time() - start,
308 'items_cold': base64.b64encode(large.pack(cold)), 336 'items_cold': base64.b64encode(large.pack(cold)),
309 'items_hot': base64.b64encode(large.pack(hot)), 337 'items_hot': base64.b64encode(large.pack(hot)),
310 } 338 }
311 return outputs_ref, success, stats 339 return outputs_ref, success, stats
312 340
313 341
314 def map_and_run( 342 def map_and_run(
315 command, isolated_hash, storage, cache, leak_temp_dir, root_dir, 343 command, isolated_hash, storage, cache, leak_temp_dir, root_dir,
316 hard_timeout, grace_period, extra_args): 344 hard_timeout, grace_period, extra_args, cipd_path, cipd_stats):
317 """Runs a command with optional isolated input/output. 345 """Runs a command with optional isolated input/output.
318 346
319 See run_tha_test for argument documentation. 347 See run_tha_test for argument documentation.
320 348
321 Returns metadata about the result. 349 Returns metadata about the result.
322 """ 350 """
323 assert bool(command) ^ bool(isolated_hash) 351 assert bool(command) ^ bool(isolated_hash)
324 result = { 352 result = {
325 'duration': None, 353 'duration': None,
326 'exit_code': None, 354 'exit_code': None,
327 'had_hard_timeout': False, 355 'had_hard_timeout': False,
328 'internal_failure': None, 356 'internal_failure': None,
329 'stats': { 357 'stats': {
330 # 'isolated': { 358 # 'isolated': {
359 # 'cipd': {
360 # 'duration': 0.,
M-A Ruel 2016/06/09 18:31:11 'get_client_duration' ?
nodir 2016/06/09 18:49:05 There is field in OperationStats for get_client_du
M-A Ruel 2016/06/09 18:52:55 No, I just mean adding it here since this value wi
nodir 2016/06/09 18:57:06 OK
nodir 2016/06/09 21:36:11 Done.
361 # },
331 # 'download': { 362 # 'download': {
332 # 'duration': 0., 363 # 'duration': 0.,
333 # 'initial_number_items': 0, 364 # 'initial_number_items': 0,
334 # 'initial_size': 0, 365 # 'initial_size': 0,
335 # 'items_cold': '<large.pack()>', 366 # 'items_cold': '<large.pack()>',
336 # 'items_hot': '<large.pack()>', 367 # 'items_hot': '<large.pack()>',
337 # }, 368 # },
338 # 'upload': { 369 # 'upload': {
339 # 'duration': 0., 370 # 'duration': 0.,
340 # 'items_cold': '<large.pack()>', 371 # 'items_cold': '<large.pack()>',
341 # 'items_hot': '<large.pack()>', 372 # 'items_hot': '<large.pack()>',
342 # }, 373 # },
343 # }, 374 # },
344 }, 375 },
345 'outputs_ref': None, 376 'outputs_ref': None,
346 'version': 4, 377 'version': 5,
347 } 378 }
379 if cipd_stats:
380 result['stats']['cipd'] = cipd_stats
381
348 if root_dir: 382 if root_dir:
349 file_path.ensure_tree(root_dir, 0700) 383 file_path.ensure_tree(root_dir, 0700)
350 prefix = u''
351 else: 384 else:
352 root_dir = os.path.dirname(cache.cache_dir) if cache.cache_dir else None 385 root_dir = os.path.dirname(cache.cache_dir) if cache.cache_dir else None
353 prefix = u'isolated_' 386 run_dir = make_temp_dir(u'isolated_run', root_dir)
354 run_dir = make_temp_dir(prefix + u'run', root_dir) 387 out_dir = make_temp_dir(u'isolated_out', root_dir) if storage else None
355 out_dir = make_temp_dir(prefix + u'out', root_dir) if storage else None 388 tmp_dir = make_temp_dir(u'isolated_tmp', root_dir)
356 tmp_dir = make_temp_dir(prefix + u'tmp', root_dir)
357 cwd = run_dir 389 cwd = run_dir
358 390
359 try: 391 try:
360 if isolated_hash: 392 if isolated_hash:
361 isolated_stats = result['stats'].setdefault('isolated', {}) 393 isolated_stats = result['stats'].setdefault('isolated', {})
362 bundle, isolated_stats['download'] = fetch_and_measure( 394 bundle, isolated_stats['download'] = fetch_and_measure(
363 isolated_hash=isolated_hash, 395 isolated_hash=isolated_hash,
364 storage=storage, 396 storage=storage,
365 cache=cache, 397 cache=cache,
366 outdir=run_dir) 398 outdir=run_dir)
367 if not bundle.command: 399 if not bundle.command:
368 # Handle this as a task failure, not an internal failure. 400 # Handle this as a task failure, not an internal failure.
369 sys.stderr.write( 401 sys.stderr.write(
370 '<The .isolated doesn\'t declare any command to run!>\n' 402 '<The .isolated doesn\'t declare any command to run!>\n'
371 '<Check your .isolate for missing \'command\' variable>\n') 403 '<Check your .isolate for missing \'command\' variable>\n')
372 if os.environ.get('SWARMING_TASK_ID'): 404 if os.environ.get('SWARMING_TASK_ID'):
373 # Give an additional hint when running as a swarming task. 405 # Give an additional hint when running as a swarming task.
374 sys.stderr.write('<This occurs at the \'isolate\' step>\n') 406 sys.stderr.write('<This occurs at the \'isolate\' step>\n')
375 result['exit_code'] = 1 407 result['exit_code'] = 1
376 return result 408 return result
377 409
378 change_tree_read_only(run_dir, bundle.read_only) 410 change_tree_read_only(run_dir, bundle.read_only)
379 cwd = os.path.normpath(os.path.join(cwd, bundle.relative_cwd)) 411 cwd = os.path.normpath(os.path.join(cwd, bundle.relative_cwd))
380 command = bundle.command + extra_args 412 command = bundle.command + extra_args
413
381 command = tools.fix_python_path(command) 414 command = tools.fix_python_path(command)
415 command = process_command(command, out_dir, cipd_path)
382 file_path.ensure_command_has_abs_path(command, cwd) 416 file_path.ensure_command_has_abs_path(command, cwd)
417
383 sys.stdout.flush() 418 sys.stdout.flush()
384 start = time.time() 419 start = time.time()
385 try: 420 try:
386 result['exit_code'], result['had_hard_timeout'] = run_command( 421 result['exit_code'], result['had_hard_timeout'] = run_command(
387 process_command(command, out_dir), cwd, tmp_dir, hard_timeout, 422 command, cwd, tmp_dir, hard_timeout, grace_period)
388 grace_period)
389 finally: 423 finally:
390 result['duration'] = max(time.time() - start, 0) 424 result['duration'] = max(time.time() - start, 0)
391 except Exception as e: 425 except Exception as e:
392 # An internal error occured. Report accordingly so the swarming task will be 426 # An internal error occured. Report accordingly so the swarming task will be
393 # retried automatically. 427 # retried automatically.
394 logging.exception('internal failure: %s', e) 428 logging.exception('internal failure: %s', e)
395 result['internal_failure'] = str(e) 429 result['internal_failure'] = str(e)
396 on_error.report(None) 430 on_error.report(None)
397 finally: 431 finally:
398 try: 432 try:
(...skipping 45 matching lines...) Expand 10 before | Expand all | Expand 10 after
444 except Exception as e: 478 except Exception as e:
445 # Swallow any exception in the main finally clause. 479 # Swallow any exception in the main finally clause.
446 if out_dir: 480 if out_dir:
447 logging.exception('Leaking out_dir %s: %s', out_dir, e) 481 logging.exception('Leaking out_dir %s: %s', out_dir, e)
448 result['internal_failure'] = str(e) 482 result['internal_failure'] = str(e)
449 return result 483 return result
450 484
451 485
452 def run_tha_test( 486 def run_tha_test(
453 command, isolated_hash, storage, cache, leak_temp_dir, result_json, 487 command, isolated_hash, storage, cache, leak_temp_dir, result_json,
454 root_dir, hard_timeout, grace_period, extra_args): 488 root_dir, hard_timeout, grace_period, extra_args, cipd_path, cipd_stats):
455 """Runs an executable and records execution metadata. 489 """Runs an executable and records execution metadata.
456 490
457 Either command or isolated_hash must be specified. 491 Either command or isolated_hash must be specified.
458 492
459 If isolated_hash is specified, downloads the dependencies in the cache, 493 If isolated_hash is specified, downloads the dependencies in the cache,
460 hardlinks them into a temporary directory and runs the command specified in 494 hardlinks them into a temporary directory and runs the command specified in
461 the .isolated. 495 the .isolated.
462 496
463 A temporary directory is created to hold the output files. The content inside 497 A temporary directory is created to hold the output files. The content inside
464 this directory will be uploaded back to |storage| packaged as a .isolated 498 this directory will be uploaded back to |storage| packaged as a .isolated
465 file. 499 file.
466 500
467 Arguments: 501 Arguments:
468 command: the command to run, a list of strings. Mutually exclusive with 502 command: the command to run, a list of strings. Mutually exclusive with
469 isolated_hash. 503 isolated_hash.
470 isolated_hash: the SHA-1 of the .isolated file that must be retrieved to 504 isolated_hash: the SHA-1 of the .isolated file that must be retrieved to
471 recreate the tree of files to run the target executable. 505 recreate the tree of files to run the target executable.
472 The command specified in the .isolated is executed. 506 The command specified in the .isolated is executed.
473 Mutually exclusive with command argument. 507 Mutually exclusive with command argument.
474 storage: an isolateserver.Storage object to retrieve remote objects. This 508 storage: an isolateserver.Storage object to retrieve remote objects. This
475 object has a reference to an isolateserver.StorageApi, which does 509 object has a reference to an isolateserver.StorageApi, which does
476 the actual I/O. 510 the actual I/O.
477 cache: an isolateserver.LocalCache to keep from retrieving the same objects 511 cache: an isolateserver.LocalCache to keep from retrieving the same objects
478 constantly by caching the objects retrieved. Can be on-disk or 512 constantly by caching the objects retrieved. Can be on-disk or
479 in-memory. 513 in-memory.
480 leak_temp_dir: if true, the temporary directory will be deliberately leaked 514 leak_temp_dir: if true, the temporary directory will be deliberately leaked
481 for later examination. 515 for later examination.
482 result_json: file path to dump result metadata into. If set, the process 516 result_json: file path to dump result metadata into. If set, the process
483 exit code is always 0 unless an internal error occured. 517 exit code is always 0 unless an internal error occurred.
484 root_dir: directory to the path to use to create the temporary directory. If 518 root_dir: directory to the path to use to create the temporary directory. If
485 not specified, a random temporary directory is created. 519 not specified, a random temporary directory is created.
486 hard_timeout: kills the process if it lasts more than this amount of 520 hard_timeout: kills the process if it lasts more than this amount of
487 seconds. 521 seconds.
488 grace_period: number of seconds to wait between SIGTERM and SIGKILL. 522 grace_period: number of seconds to wait between SIGTERM and SIGKILL.
489 extra_args: optional arguments to add to the command stated in the .isolate 523 extra_args: optional arguments to add to the command stated in the .isolate
490 file. Ignored if isolate_hash is empty. 524 file. Ignored if isolate_hash is empty.
525 cipd_path: value for CIPD_PATH_PARAMETER. If empty, command or extra_args
526 must not use CIPD_PATH_PARAMETER.
527 cipd_stats: CIPD stats to include in the metadata written to result_json.
491 528
492 Returns: 529 Returns:
493 Process exit code that should be used. 530 Process exit code that should be used.
494 """ 531 """
495 assert bool(command) ^ bool(isolated_hash) 532 assert bool(command) ^ bool(isolated_hash)
496 extra_args = extra_args or [] 533 extra_args = extra_args or []
534
497 if any(ISOLATED_OUTDIR_PARAMETER in a for a in (command or extra_args)): 535 if any(ISOLATED_OUTDIR_PARAMETER in a for a in (command or extra_args)):
498 assert storage is not None, 'storage is None although outdir is specified' 536 assert storage is not None, 'storage is None although outdir is specified'
499 537
500 if result_json: 538 if result_json:
501 # Write a json output file right away in case we get killed. 539 # Write a json output file right away in case we get killed.
502 result = { 540 result = {
503 'exit_code': None, 541 'exit_code': None,
504 'had_hard_timeout': False, 542 'had_hard_timeout': False,
505 'internal_failure': 'Was terminated before completion', 543 'internal_failure': 'Was terminated before completion',
506 'outputs_ref': None, 544 'outputs_ref': None,
507 'version': 2, 545 'version': 5,
508 } 546 }
547 if cipd_stats:
548 result['stats'] = {'cipd': cipd_stats}
509 tools.write_json(result_json, result, dense=True) 549 tools.write_json(result_json, result, dense=True)
510 550
511 # run_isolated exit code. Depends on if result_json is used or not. 551 # run_isolated exit code. Depends on if result_json is used or not.
512 result = map_and_run( 552 result = map_and_run(
513 command, isolated_hash, storage, cache, leak_temp_dir, root_dir, 553 command, isolated_hash, storage, cache, leak_temp_dir, root_dir,
514 hard_timeout, grace_period, extra_args) 554 hard_timeout, grace_period, extra_args, cipd_path, cipd_stats)
515 logging.info('Result:\n%s', tools.format_json(result, dense=True)) 555 logging.info('Result:\n%s', tools.format_json(result, dense=True))
516 if result_json: 556 if result_json:
517 # We've found tests to delete 'work' when quitting, causing an exception 557 # We've found tests to delete 'work' when quitting, causing an exception
518 # here. Try to recreate the directory if necessary. 558 # here. Try to recreate the directory if necessary.
519 file_path.ensure_tree(os.path.dirname(result_json)) 559 file_path.ensure_tree(os.path.dirname(result_json))
520 tools.write_json(result_json, result, dense=True) 560 tools.write_json(result_json, result, dense=True)
521 # Only return 1 if there was an internal error. 561 # Only return 1 if there was an internal error.
522 return int(bool(result['internal_failure'])) 562 return int(bool(result['internal_failure']))
523 563
524 # Marshall into old-style inline output. 564 # Marshall into old-style inline output.
525 if result['outputs_ref']: 565 if result['outputs_ref']:
526 data = { 566 data = {
527 'hash': result['outputs_ref']['isolated'], 567 'hash': result['outputs_ref']['isolated'],
528 'namespace': result['outputs_ref']['namespace'], 568 'namespace': result['outputs_ref']['namespace'],
529 'storage': result['outputs_ref']['isolatedserver'], 569 'storage': result['outputs_ref']['isolatedserver'],
530 } 570 }
531 sys.stdout.flush() 571 sys.stdout.flush()
532 print( 572 print(
533 '[run_isolated_out_hack]%s[/run_isolated_out_hack]' % 573 '[run_isolated_out_hack]%s[/run_isolated_out_hack]' %
534 tools.format_json(data, dense=True)) 574 tools.format_json(data, dense=True))
535 sys.stdout.flush() 575 sys.stdout.flush()
536 return result['exit_code'] or int(bool(result['internal_failure'])) 576 return result['exit_code'] or int(bool(result['internal_failure']))
537 577
538 578
539 def main(args): 579 @contextlib.contextmanager
580 def ensure_packages(
581 site_root, packages, service_url, client_package, cache_dir=None,
582 timeout=None):
583 """Returns a context manager that installs packages into site_root.
584
585 Creates/recreates site_root dir and install packages before yielding.
586 After yielding deletes site_root dir.
587
588 Args:
589 site_root (str): where to install the packages.
590 packages (list of str): list of package to ensure.
591 Package format is same as for "--cipd-package" option.
592 service_url (str): CIPD server url, e.g.
593 "https://chrome-infra-packages.appspot.com."
594 client_package (str): CIPD package of CIPD client.
595 Format is same as for "--cipd-package" option.
596 cache_dir (str): where to keep cache of cipd clients, packages and tags.
597 timeout: max duration in seconds that this function can take.
598
599 Yields:
600 CIPD stats as dict.
601 """
602 assert cache_dir
603 timeoutfn = tools.sliding_timeout(timeout)
604 if not packages:
605 yield
606 return
607
608 start = time.time()
609
610 cache_dir = os.path.abspath(cache_dir)
611
612 # Get CIPD client.
613 client_package_name, client_version = cipd.parse_package(client_package)
614 get_client_start = time.time()
615 client_manager = cipd.get_client(
616 service_url, client_package_name, client_version, cache_dir,
617 timeout=timeoutfn())
618 with client_manager as client:
619 get_client_duration = time.time() - get_client_start
620 # Create site_root, install packages, yield, delete site_root.
621 if fs.isdir(site_root):
622 file_path.rmtree(site_root)
623 file_path.ensure_tree(site_root, 0770)
624 file_path.make_tree_writeable(site_root)
M-A Ruel 2016/06/09 18:31:11 This function call is not needed, since the tree w
nodir 2016/06/09 21:36:11 Done.
625 try:
626 client.ensure(
627 site_root, packages,
628 cache_dir=os.path.join(cache_dir, 'cipd_internal'),
629 timeout=timeoutfn())
630
631 total_duration = time.time() - start
632 logging.info(
633 'Installing CIPD client and packages took %d seconds', total_duration)
634
635 file_path.make_tree_files_read_only(site_root)
636 yield {
637 'duration': total_duration,
638 'get_client_duration': get_client_duration,
639 }
640 finally:
641 file_path.rmtree(site_root)
642
643
644 def create_option_parser():
540 parser = logging_utils.OptionParserWithLogging( 645 parser = logging_utils.OptionParserWithLogging(
541 usage='%prog <options> [command to run or extra args]', 646 usage='%prog <options> [command to run or extra args]',
542 version=__version__, 647 version=__version__,
543 log_file=RUN_ISOLATED_LOG_FILE) 648 log_file=RUN_ISOLATED_LOG_FILE)
544 parser.add_option( 649 parser.add_option(
545 '--clean', action='store_true', 650 '--clean', action='store_true',
546 help='Cleans the cache, trimming it necessary and remove corrupted items ' 651 help='Cleans the cache, trimming it necessary and remove corrupted items '
547 'and returns without executing anything; use with -v to know what ' 652 'and returns without executing anything; use with -v to know what '
548 'was done') 653 'was done')
549 parser.add_option( 654 parser.add_option(
550 '--json', 655 '--json',
551 help='dump output metadata to json file. When used, run_isolated returns ' 656 help='dump output metadata to json file. When used, run_isolated returns '
552 'non-zero only on internal failure') 657 'non-zero only on internal failure')
553 parser.add_option( 658 parser.add_option(
554 '--hard-timeout', type='float', help='Enforce hard timeout in execution') 659 '--hard-timeout', type='float', help='Enforce hard timeout in execution')
555 parser.add_option( 660 parser.add_option(
556 '--grace-period', type='float', 661 '--grace-period', type='float',
557 help='Grace period between SIGTERM and SIGKILL') 662 help='Grace period between SIGTERM and SIGKILL')
558 data_group = optparse.OptionGroup(parser, 'Data source') 663 data_group = optparse.OptionGroup(parser, 'Data source')
559 data_group.add_option( 664 data_group.add_option(
560 '-s', '--isolated', 665 '-s', '--isolated',
561 help='Hash of the .isolated to grab from the isolate server.') 666 help='Hash of the .isolated to grab from the isolate server.')
562 isolateserver.add_isolate_server_options(data_group) 667 isolateserver.add_isolate_server_options(data_group)
563 parser.add_option_group(data_group) 668 parser.add_option_group(data_group)
564 669
565 isolateserver.add_cache_options(parser) 670 isolateserver.add_cache_options(parser)
566 parser.set_defaults(cache='cache') 671
672 cipd.add_cipd_options(parser)
567 673
568 debug_group = optparse.OptionGroup(parser, 'Debugging') 674 debug_group = optparse.OptionGroup(parser, 'Debugging')
569 debug_group.add_option( 675 debug_group.add_option(
570 '--leak-temp-dir', 676 '--leak-temp-dir',
571 action='store_true', 677 action='store_true',
572 help='Deliberately leak isolate\'s temp dir for later examination ' 678 help='Deliberately leak isolate\'s temp dir for later examination. '
573 '[default: %default]') 679 'Default: %default')
574 debug_group.add_option( 680 debug_group.add_option(
575 '--root-dir', help='Use a directory instead of a random one') 681 '--root-dir', help='Use a directory instead of a random one')
576 parser.add_option_group(debug_group) 682 parser.add_option_group(debug_group)
577 683
578 auth.add_auth_options(parser) 684 auth.add_auth_options(parser)
685
686 parser.set_defaults(cache='cache', cipd_cache='cipd_cache')
687 return parser
688
689
690 def main(args):
691 parser = create_option_parser()
579 options, args = parser.parse_args(args) 692 options, args = parser.parse_args(args)
580 693
581 cache = isolateserver.process_cache_options(options) 694 cache = isolateserver.process_cache_options(options)
582 if options.clean: 695 if options.clean:
583 if options.isolated: 696 if options.isolated:
584 parser.error('Can\'t use --isolated with --clean.') 697 parser.error('Can\'t use --isolated with --clean.')
585 if options.isolate_server: 698 if options.isolate_server:
586 parser.error('Can\'t use --isolate-server with --clean.') 699 parser.error('Can\'t use --isolate-server with --clean.')
587 if options.json: 700 if options.json:
588 parser.error('Can\'t use --json with --clean.') 701 parser.error('Can\'t use --json with --clean.')
589 cache.cleanup() 702 cache.cleanup()
590 return 0 703 return 0
591 704
592 if not options.isolated and not args: 705 if not options.isolated and not args:
593 parser.error('--isolated or command to run is required.') 706 parser.error('--isolated or command to run is required.')
594 707
595 auth.process_auth_options(parser, options) 708 auth.process_auth_options(parser, options)
596 709
597 isolateserver.process_isolate_server_options( 710 isolateserver.process_isolate_server_options(
598 parser, options, True, False) 711 parser, options, True, False)
599 if not options.isolate_server: 712 if not options.isolate_server:
600 if options.isolated: 713 if options.isolated:
601 parser.error('--isolated requires --isolate-server') 714 parser.error('--isolated requires --isolate-server')
602 if ISOLATED_OUTDIR_PARAMETER in args: 715 if ISOLATED_OUTDIR_PARAMETER in args:
603 parser.error( 716 parser.error(
604 '%s in args requires --isolate-server' % ISOLATED_OUTDIR_PARAMETER) 717 '%s in args requires --isolate-server' % ISOLATED_OUTDIR_PARAMETER)
605 718
606 if options.root_dir:
607 options.root_dir = unicode(os.path.abspath(options.root_dir))
608 if options.json: 719 if options.json:
609 options.json = unicode(os.path.abspath(options.json)) 720 options.json = unicode(os.path.abspath(options.json))
610 721
611 command = [] if options.isolated else args 722 cipd.validate_cipd_options(parser, options)
612 if options.isolate_server: 723
613 storage = isolateserver.get_storage( 724 root_dir = options.root_dir
614 options.isolate_server, options.namespace) 725 if root_dir:
615 # Hashing schemes used by |storage| and |cache| MUST match. 726 root_dir = unicode(os.path.abspath(root_dir))
616 with storage: 727 file_path.ensure_tree(root_dir, 0700)
617 assert storage.hash_algo == cache.hash_algo
618 return run_tha_test(
619 command, options.isolated, storage, cache, options.leak_temp_dir,
620 options.json, options.root_dir, options.hard_timeout,
621 options.grace_period, args)
622 else: 728 else:
623 return run_tha_test( 729 root_dir = os.path.dirname(cache.cache_dir) if cache.cache_dir else None
624 command, options.isolated, None, cache, options.leak_temp_dir, 730
625 options.json, options.root_dir, options.hard_timeout, 731 cipd_path = None
626 options.grace_period, args) 732 if not options.cipd_package:
733 if CIPD_PATH_PARAMETER in args:
734 parser.error('%s in args requires --cipd-package' % CIPD_PATH_PARAMETER)
735 else:
736 cipd_path = make_temp_dir(u'cipd_site_root', root_dir)
737
738 try:
739 with ensure_packages(
740 cipd_path, options.cipd_package, options.cipd_server,
741 options.cipd_client_package, options.cipd_cache) as cipd_stats:
742 command = [] if options.isolated else args
743 if options.isolate_server:
744 storage = isolateserver.get_storage(
745 options.isolate_server, options.namespace)
746 with storage:
747 # Hashing schemes used by |storage| and |cache| MUST match.
748 assert storage.hash_algo == cache.hash_algo
749 return run_tha_test(
750 command, options.isolated, storage, cache, options.leak_temp_dir,
751 options.json, root_dir, options.hard_timeout,
752 options.grace_period, args, cipd_path, cipd_stats)
753 else:
754 return run_tha_test(
755 command, options.isolated, None, cache, options.leak_temp_dir,
756 options.json, root_dir, options.hard_timeout,
757 options.grace_period, args, cipd_path, cipd_stats)
758 except cipd.Error as ex:
759 print >> sys.stderr, ex.message
760 return 1
627 761
628 762
629 if __name__ == '__main__': 763 if __name__ == '__main__':
630 subprocess42.inhibit_os_error_reporting() 764 subprocess42.inhibit_os_error_reporting()
631 # Ensure that we are always running with the correct encoding. 765 # Ensure that we are always running with the correct encoding.
632 fix_encoding.fix_encoding() 766 fix_encoding.fix_encoding()
633 sys.exit(main(sys.argv[1:])) 767 sys.exit(main(sys.argv[1:]))
OLDNEW
« client/cipd.py ('K') | « client/isolateserver.py ('k') | client/swarming.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698