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

Unified 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: fix client fetching 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 side-by-side diff with in-line comments
Download patch
« no previous file with comments | « client/isolateserver.py ('k') | client/swarming.py » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: client/run_isolated.py
diff --git a/client/run_isolated.py b/client/run_isolated.py
index 43276449eade74f6108754febbb740d555e68c8a..a94808947cb35a85f0c10f6d316f155f02bafa8d 100755
--- a/client/run_isolated.py
+++ b/client/run_isolated.py
@@ -13,15 +13,24 @@ appends args to the command in the fetched isolated and runs it.
To improve performance, keeps a local cache.
The local cache can safely be deleted.
+Any ${EXECUTABLE_SUFFIX} on the command line will be replaced with ".exe" string
+on Windows and "" on other platforms.
+
+If at least one CIPD package was specified, any ${CIPD_PATH} on the command line
+will be replaced by location of a temporary directory that contains installed
+packages.
+
Any ${ISOLATED_OUTDIR} on the command line will be replaced by the location of a
temporary directory upon execution of the command specified in the .isolated
file. All content written to this directory will be uploaded upon termination
and the .isolated file describing this directory will be printed to stdout.
"""
-__version__ = '0.7.0'
+__version__ = '0.8.0'
import base64
+import contextlib
+import hashlib
import logging
import optparse
import os
@@ -41,10 +50,13 @@ from utils import tools
from utils import zip_package
import auth
+import cipd
import isolateserver
ISOLATED_OUTDIR_PARAMETER = '${ISOLATED_OUTDIR}'
+CIPD_PATH_PARAMETER = '${CIPD_PATH}'
+EXECUTABLE_SUFFIX_PARAMETER = '${EXECUTABLE_SUFFIX}'
# Absolute path to this file (can be None if running from zip on Mac).
THIS_FILE_PATH = os.path.abspath(__file__) if __file__ else None
@@ -84,6 +96,7 @@ def get_as_zip_package(executable=True):
package.add_python_file(os.path.join(BASE_DIR, 'isolated_format.py'))
package.add_python_file(os.path.join(BASE_DIR, 'isolateserver.py'))
package.add_python_file(os.path.join(BASE_DIR, 'auth.py'))
+ package.add_python_file(os.path.join(BASE_DIR, 'cipd.py'))
package.add_directory(os.path.join(BASE_DIR, 'third_party'))
package.add_directory(os.path.join(BASE_DIR, 'utils'))
return package
@@ -140,13 +153,28 @@ def change_tree_read_only(rootdir, read_only):
(rootdir, read_only, read_only))
-def process_command(command, out_dir):
- """Replaces isolated specific variables in a command line."""
+def process_command(command, out_dir, cipd_path):
+ """Replaces variables in a command line.
+
+ Raises:
+ ValueError if a parameter is requested in |command| but its value is not
+ provided.
+ """
def fix(arg):
+ arg = arg.replace(EXECUTABLE_SUFFIX_PARAMETER, cipd.EXECUTABLE_SUFFIX)
+ replace_slash = False
+ if CIPD_PATH_PARAMETER in arg:
+ if not cipd_path:
+ raise ValueError('cipd_path is requested in command, but not provided')
+ arg = arg.replace(CIPD_PATH_PARAMETER, cipd_path)
+ replace_slash = True
if ISOLATED_OUTDIR_PARAMETER in arg:
- assert out_dir
+ if not out_dir:
+ raise ValueError('out_dir is requested in command, but not provided')
arg = arg.replace(ISOLATED_OUTDIR_PARAMETER, out_dir)
- # Replace slashes only if ISOLATED_OUTDIR_PARAMETER is present
+ replace_slash = True
+ if replace_slash:
+ # Replace slashes only if parameters are present
# because of arguments like '${ISOLATED_OUTDIR}/foo/bar'
arg = arg.replace('/', os.sep)
return arg
@@ -313,7 +341,7 @@ def delete_and_upload(storage, out_dir, leak_temp_dir):
def map_and_run(
command, isolated_hash, storage, cache, leak_temp_dir, root_dir,
- hard_timeout, grace_period, extra_args):
+ hard_timeout, grace_period, extra_args, cipd_path, cipd_stats):
"""Runs a command with optional isolated input/output.
See run_tha_test for argument documentation.
@@ -328,6 +356,10 @@ def map_and_run(
'internal_failure': None,
'stats': {
# 'isolated': {
+ # 'cipd': {
+ # 'duration': 0.,
+ # 'get_client_duration': 0.,
+ # },
# 'download': {
# 'duration': 0.,
# 'initial_number_items': 0,
@@ -343,17 +375,18 @@ def map_and_run(
# },
},
'outputs_ref': None,
- 'version': 4,
+ 'version': 5,
}
+ if cipd_stats:
+ result['stats']['cipd'] = cipd_stats
+
if root_dir:
file_path.ensure_tree(root_dir, 0700)
- prefix = u''
else:
root_dir = os.path.dirname(cache.cache_dir) if cache.cache_dir else None
- prefix = u'isolated_'
- run_dir = make_temp_dir(prefix + u'run', root_dir)
- out_dir = make_temp_dir(prefix + u'out', root_dir) if storage else None
- tmp_dir = make_temp_dir(prefix + u'tmp', root_dir)
+ run_dir = make_temp_dir(u'isolated_run', root_dir)
+ out_dir = make_temp_dir(u'isolated_out', root_dir) if storage else None
+ tmp_dir = make_temp_dir(u'isolated_tmp', root_dir)
cwd = run_dir
try:
@@ -378,14 +411,16 @@ def map_and_run(
change_tree_read_only(run_dir, bundle.read_only)
cwd = os.path.normpath(os.path.join(cwd, bundle.relative_cwd))
command = bundle.command + extra_args
+
command = tools.fix_python_path(command)
+ command = process_command(command, out_dir, cipd_path)
file_path.ensure_command_has_abs_path(command, cwd)
+
sys.stdout.flush()
start = time.time()
try:
result['exit_code'], result['had_hard_timeout'] = run_command(
- process_command(command, out_dir), cwd, tmp_dir, hard_timeout,
- grace_period)
+ command, cwd, tmp_dir, hard_timeout, grace_period)
finally:
result['duration'] = max(time.time() - start, 0)
except Exception as e:
@@ -451,7 +486,7 @@ def map_and_run(
def run_tha_test(
command, isolated_hash, storage, cache, leak_temp_dir, result_json,
- root_dir, hard_timeout, grace_period, extra_args):
+ root_dir, hard_timeout, grace_period, extra_args, cipd_path, cipd_stats):
"""Runs an executable and records execution metadata.
Either command or isolated_hash must be specified.
@@ -480,7 +515,7 @@ def run_tha_test(
leak_temp_dir: if true, the temporary directory will be deliberately leaked
for later examination.
result_json: file path to dump result metadata into. If set, the process
- exit code is always 0 unless an internal error occured.
+ exit code is always 0 unless an internal error occurred.
root_dir: directory to the path to use to create the temporary directory. If
not specified, a random temporary directory is created.
hard_timeout: kills the process if it lasts more than this amount of
@@ -488,12 +523,16 @@ def run_tha_test(
grace_period: number of seconds to wait between SIGTERM and SIGKILL.
extra_args: optional arguments to add to the command stated in the .isolate
file. Ignored if isolate_hash is empty.
+ cipd_path: value for CIPD_PATH_PARAMETER. If empty, command or extra_args
+ must not use CIPD_PATH_PARAMETER.
+ cipd_stats: CIPD stats to include in the metadata written to result_json.
Returns:
Process exit code that should be used.
"""
assert bool(command) ^ bool(isolated_hash)
extra_args = extra_args or []
+
if any(ISOLATED_OUTDIR_PARAMETER in a for a in (command or extra_args)):
assert storage is not None, 'storage is None although outdir is specified'
@@ -504,14 +543,16 @@ def run_tha_test(
'had_hard_timeout': False,
'internal_failure': 'Was terminated before completion',
'outputs_ref': None,
- 'version': 2,
+ 'version': 5,
}
+ if cipd_stats:
+ result['stats'] = {'cipd': cipd_stats}
tools.write_json(result_json, result, dense=True)
# run_isolated exit code. Depends on if result_json is used or not.
result = map_and_run(
command, isolated_hash, storage, cache, leak_temp_dir, root_dir,
- hard_timeout, grace_period, extra_args)
+ hard_timeout, grace_period, extra_args, cipd_path, cipd_stats)
logging.info('Result:\n%s', tools.format_json(result, dense=True))
if result_json:
# We've found tests to delete 'work' when quitting, causing an exception
@@ -536,7 +577,71 @@ def run_tha_test(
return result['exit_code'] or int(bool(result['internal_failure']))
-def main(args):
+@contextlib.contextmanager
+def ensure_packages(
+ site_root, packages, service_url, client_package, cache_dir=None,
+ timeout=None):
+ """Returns a context manager that installs packages into site_root.
+
+ Creates/recreates site_root dir and install packages before yielding.
+ After yielding deletes site_root dir.
+
+ Args:
+ site_root (str): where to install the packages.
+ packages (list of str): list of package to ensure.
+ Package format is same as for "--cipd-package" option.
+ service_url (str): CIPD server url, e.g.
+ "https://chrome-infra-packages.appspot.com."
+ client_package (str): CIPD package of CIPD client.
+ Format is same as for "--cipd-package" option.
+ cache_dir (str): where to keep cache of cipd clients, packages and tags.
+ timeout: max duration in seconds that this function can take.
+
+ Yields:
+ CIPD stats as dict.
+ """
+ assert cache_dir
+ timeoutfn = tools.sliding_timeout(timeout)
+ if not packages:
+ yield
+ return
+
+ start = time.time()
+
+ cache_dir = os.path.abspath(cache_dir)
+
+ # Get CIPD client.
+ client_package_name, client_version = cipd.parse_package(client_package)
+ get_client_start = time.time()
+ client_manager = cipd.get_client(
+ service_url, client_package_name, client_version, cache_dir,
+ timeout=timeoutfn())
+ with client_manager as client:
+ get_client_duration = time.time() - get_client_start
+ # Create site_root, install packages, yield, delete site_root.
+ if fs.isdir(site_root):
+ file_path.rmtree(site_root)
+ file_path.ensure_tree(site_root, 0770)
+ try:
+ client.ensure(
+ site_root, packages,
+ cache_dir=os.path.join(cache_dir, 'cipd_internal'),
+ timeout=timeoutfn())
+
+ total_duration = time.time() - start
+ logging.info(
+ 'Installing CIPD client and packages took %d seconds', total_duration)
+
+ file_path.make_tree_files_read_only(site_root)
+ yield {
+ 'duration': total_duration,
+ 'get_client_duration': get_client_duration,
+ }
+ finally:
+ file_path.rmtree(site_root)
+
+
+def create_option_parser():
parser = logging_utils.OptionParserWithLogging(
usage='%prog <options> [command to run or extra args]',
version=__version__,
@@ -563,19 +668,27 @@ def main(args):
parser.add_option_group(data_group)
isolateserver.add_cache_options(parser)
- parser.set_defaults(cache='cache')
+
+ cipd.add_cipd_options(parser)
debug_group = optparse.OptionGroup(parser, 'Debugging')
debug_group.add_option(
'--leak-temp-dir',
action='store_true',
- help='Deliberately leak isolate\'s temp dir for later examination '
- '[default: %default]')
+ help='Deliberately leak isolate\'s temp dir for later examination. '
+ 'Default: %default')
debug_group.add_option(
'--root-dir', help='Use a directory instead of a random one')
parser.add_option_group(debug_group)
auth.add_auth_options(parser)
+
+ parser.set_defaults(cache='cache', cipd_cache='cipd_cache')
+ return parser
+
+
+def main(args):
+ parser = create_option_parser()
options, args = parser.parse_args(args)
cache = isolateserver.process_cache_options(options)
@@ -603,27 +716,48 @@ def main(args):
parser.error(
'%s in args requires --isolate-server' % ISOLATED_OUTDIR_PARAMETER)
- if options.root_dir:
- options.root_dir = unicode(os.path.abspath(options.root_dir))
if options.json:
options.json = unicode(os.path.abspath(options.json))
- command = [] if options.isolated else args
- if options.isolate_server:
- storage = isolateserver.get_storage(
- options.isolate_server, options.namespace)
- # Hashing schemes used by |storage| and |cache| MUST match.
- with storage:
- assert storage.hash_algo == cache.hash_algo
- return run_tha_test(
- command, options.isolated, storage, cache, options.leak_temp_dir,
- options.json, options.root_dir, options.hard_timeout,
- options.grace_period, args)
+ cipd.validate_cipd_options(parser, options)
+
+ root_dir = options.root_dir
+ if root_dir:
+ root_dir = unicode(os.path.abspath(root_dir))
+ file_path.ensure_tree(root_dir, 0700)
+ else:
+ root_dir = os.path.dirname(cache.cache_dir) if cache.cache_dir else None
+
+ cipd_path = None
+ if not options.cipd_package:
+ if CIPD_PATH_PARAMETER in args:
+ parser.error('%s in args requires --cipd-package' % CIPD_PATH_PARAMETER)
else:
- return run_tha_test(
- command, options.isolated, None, cache, options.leak_temp_dir,
- options.json, options.root_dir, options.hard_timeout,
- options.grace_period, args)
+ cipd_path = make_temp_dir(u'cipd_site_root', root_dir)
+
+ try:
+ with ensure_packages(
+ cipd_path, options.cipd_package, options.cipd_server,
+ options.cipd_client_package, options.cipd_cache) as cipd_stats:
+ command = [] if options.isolated else args
+ if options.isolate_server:
+ storage = isolateserver.get_storage(
+ options.isolate_server, options.namespace)
+ with storage:
+ # Hashing schemes used by |storage| and |cache| MUST match.
+ assert storage.hash_algo == cache.hash_algo
+ return run_tha_test(
+ command, options.isolated, storage, cache, options.leak_temp_dir,
+ options.json, root_dir, options.hard_timeout,
+ options.grace_period, args, cipd_path, cipd_stats)
+ else:
+ return run_tha_test(
+ command, options.isolated, None, cache, options.leak_temp_dir,
+ options.json, root_dir, options.hard_timeout,
+ options.grace_period, args, cipd_path, cipd_stats)
+ except cipd.Error as ex:
+ print >> sys.stderr, ex.message
+ return 1
if __name__ == '__main__':
« no previous file with comments | « client/isolateserver.py ('k') | client/swarming.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698