Index: scripts/slave/launcher.py |
diff --git a/scripts/slave/launcher.py b/scripts/slave/launcher.py |
new file mode 100755 |
index 0000000000000000000000000000000000000000..970c7447b3de46b7f31ad35dd1a397974454f276 |
--- /dev/null |
+++ b/scripts/slave/launcher.py |
@@ -0,0 +1,418 @@ |
+#!/usr/bin/env python |
+# Copyright (c) 2013 The Chromium Authors. All rights reserved. |
+# Use of this source code is governed by a BSD-style license that can be |
+# found in the LICENSE file. |
+ |
+import argparse |
+import collections |
+import contextlib |
+import datetime |
+import hashlib |
+import itertools |
+import logging |
+import os |
+import platform |
+import shutil |
+import subprocess |
+import sys |
+import tempfile |
+ |
+ |
+# Install Infra environment. |
+BUILD_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, |
+ os.pardir)) |
+SCRIPTS = os.path.join(BUILD_ROOT, 'scripts') |
+sys.path.insert(0, SCRIPTS) |
+import common.env |
+common.env.Install() |
+ |
+# Useful to test if we're running on a GCE instance. |
+from gerrit_util import GceAuthenticator |
+ |
+PubSubConfig = collections.namedtuple('PubSubConfig', ('project', 'topic')) |
+ |
+# Path to the `annotated_run.py` executable. |
+ANNOTATED_RUN = os.path.join(BUILD_ROOT, 'scripts', 'slave', 'annotated_run.py') |
+ |
+# Global logger for launcher. |
+LOGGER = logging.getLogger('launcher') |
+ |
+ |
+# Return codes used by Butler/Annotee to indicate their failure (as opposed to |
+# a forwarded return code from the underlying process). |
+LOGDOG_ERROR_RETURNCODES = ( |
+ # Butler runtime error. |
+ 250, |
+ # Annotee runtime error. |
+ 251, |
+) |
+ |
+ |
+WHITELIST_MASTER_BUILDERS = { |
+} |
+ |
+# Sentinel value that, if present in master config, matches all builders |
+# underneath that master. |
+WHITELIST_ALL = '*' |
+ |
+# Environment will probe this for values. |
+# - First, (), |
+# - Then, (system,) |
+# - Then, (system, platform) |
+PLATFORM_CONFIG = { |
+ (): { |
+ 'pubsub': PubSubConfig( |
+ project='luci-logdog', |
+ topic='chrome-infra-beta', |
+ ), |
+ }, |
+ ('Linux',): { |
+ 'cipd_static_paths': ( |
+ # XXX: Get this right? |
+ '/opt/chrome-infra', |
+ ), |
+ 'credential_paths': ( |
+ # XXX: Get this right? |
+ '/opt/infra/service_accounts', |
+ ), |
+ }, |
+ ('Linux', 'x86_64'): { |
+ 'cipd_packages': { |
+ 'infra/tools/luci/logdog/butler/linux-amd64': 'testing', |
+ 'infra/tools/luci/logdog/annotee/linux-amd64': 'testing', |
+ }, |
+ }, |
+} |
+ |
+ |
+class LogDogNotBootstrapped(Exception): |
+ pass |
+ |
+ |
+class LogDogBootstrapError(Exception): |
+ pass |
+ |
+ |
+def is_executable(path): |
+ return os.path.isfile(path) and os.access(path, os.X_OK) |
+ |
+ |
+def _assert_executable(path): |
+ if not is_executable(path): |
+ raise LogDogNotBootstrapped('File is not executable: %s' % (path,)) |
+ return path |
+ |
+ |
+def ensure_directory(path): |
+ if not os.path.isdir(path): |
+ os.makedirs(path) |
+ |
+ |
+def _run_command(cmd, dry_run=False): |
+ LOGGER.info('Executing command: %s', cmd) |
+ if dry_run: |
+ LOGGER.info('(Dry Run) Not executing command.') |
+ return 0, '' |
+ proc = subprocess.Popen(cmd, stderr=subprocess.STDOUT) |
+ stdout, _ = proc.communicate() |
+ |
+ LOGGER.debug('Process [%s] output:\n%s', cmd, stdout) |
+ return proc.returncode, stdout |
+ |
+ |
+def _check_command(*args, **kwargs): |
+ rv, stdout = _run_command(args, **kwargs) |
+ if rv != 0: |
+ raise ValueError('Process exited with non-zero return code (%d)' % (rv,)) |
+ return stdout |
+ |
+ |
+class Environment(object): |
+ """Environment is the runtime enviornment of the LogDog bootstrap.""" |
+ |
+ _SENTINEL = object() |
+ |
+ def __init__(self, current_platform, **kwargs): |
+ self._platform = current_platform |
+ self._fields = kwargs |
+ |
+ @classmethod |
+ @contextlib.contextmanager |
+ def probe(cls, environ, **kw): |
+ kw.update({ |
+ 'mastername': environ.get('BUILDBOT_MASTERNAME'), |
+ 'buildername': environ.get('BUILDBOT_BUILDERNAME'), |
+ 'slavename': environ.get('BUILDBOT_SLAVENAME'), |
+ 'buildnumber': environ.get('BUILDBOT_BUILDNUMBER'), |
+ 'system': platform.system(), |
+ 'processor': platform.processor(), |
+ }) |
+ |
+ tdir = None |
+ try: |
+ tdir = tempfile.mkdtemp() |
+ LOGGER.debug('Using temporary directory [%s].', tdir) |
+ |
+ kw['tempdir'] = tdir |
+ yield cls((kw['system'], kw['processor']), **kw) |
+ finally: |
+ if tdir and os.path.isdir(tdir): |
+ LOGGER.debug('Cleaning up temporary directory [%s].', tdir) |
+ try: |
+ shutil.rmtree(tdir) |
+ except Exception: |
+ LOGGER.exception('Failed to clean up temporary directory [%s].', tdir) |
+ |
+ def __getattr__(self, key): |
+ value = getattr(self, 'get')(key) |
+ if value is not self._SENTINEL: |
+ return value |
+ raise KeyError(key) |
+ |
+ def get(self, key, default=None): |
+ value = getattr(super(Environment, self), key, self._SENTINEL) |
+ if value is not self._SENTINEL: |
+ return value |
+ |
+ value = self._fields.get(key, self._SENTINEL) |
+ if value is not self._SENTINEL: |
+ return value |
+ |
+ for i in xrange(len(self._platform)+1): |
+ pcfg = PLATFORM_CONFIG.get(self._platform[:i], {}) |
+ value = pcfg.get(key, self._SENTINEL) |
+ if value is not self._SENTINEL: |
+ return value |
+ return default |
+ |
+ def __str__(self): |
+ return str(self._fields) |
+ |
+ def is_posix(self): |
+ """Returns (bool): Whether the current system is a POSIX system.""" |
+ return self.system in ('Linux', 'Darwin') |
+ |
+ def assert_whitelisted(self): |
+ # Key on mastername. |
+ bdict = WHITELIST_MASTER_BUILDERS.get(self.mastername) |
+ if bdict is not None: |
+ # Key on buildername. |
+ if WHITELIST_ALL in bdict or self.buildername in bdict: |
+ LOGGER.info('Whitelisted master %(mastername)s, builder ' |
+ '%(buildername)s.' % self) |
+ return |
+ |
+ raise LogDogNotBootstrapped('Master %s, builder %s is not whitelisted.' % ( |
+ self.mastername, self.buildername)) |
+ |
+ @property |
+ def cipd_root(self): |
+ return os.path.join(self.tempdir, 'cipd_root') |
+ |
+ def butler_streamserver_uri(self): |
+ """Returns (str): The Butler StreamServer URI argument.""" |
+ return 'unix:%s' % (os.path.join(self.tempdir, 'butler.sock')) |
+ |
+ |
+class CIPD(object): |
+ |
+ _CIPD_NAME = 'cipd' |
+ |
+ def __init__(self, path, root): |
+ self._cipd_path = path |
+ self._root = root |
+ |
+ @classmethod |
+ def find(cls, env): |
+ for p in itertools.chain( |
+ iter(os.environ.get('PATH').split(os.pathsep)), |
+ env.cipd_static_paths): |
+ candidate = os.path.join(p, cls._CIPD_NAME) |
+ if os.path.isfile(candidate) and os.access(candidate, os.X_OK): |
+ return cls(candidate, env.cipd_root) |
+ return None |
+ |
+ def __call__(self, *args): |
+ cmd = [self._cipd_path] |
+ cmd.extend(args) |
+ _check_command(*cmd) |
+ |
+ def path(self, *components): |
+ return os.path.join(self._root, *components) |
+ |
+ def ensure(self, **packages): |
+ if len(packages) == 0: |
+ return |
+ |
+ # Emit package list. |
+ package_list = self.path('package_list.txt') |
+ lines = [ |
+ '# Automatically generated CIPD package list (launcher.py)', |
+ '# Generated at: %s' % (datetime.datetime.now().isoformat(),), |
+ '', |
+ ] |
+ for pkg, version in sorted(packages.iteritems()): |
+ lines.append('%s %s' % (pkg, version)) |
+ |
+ ensure_directory(self._root) |
+ with open(package_list, 'w+') as fd: |
+ fd.write('\n'.join(lines)) |
+ |
+ # Ensure against the package list. |
+ args = [ |
+ 'ensure', |
+ '-root', self._root, |
+ '-list', package_list, |
+ ] |
+ self(*args) |
+ |
+ |
+def _get_service_account_json(env): |
+ """Returns (str/None): If specified, the path to the service account JSON. |
+ |
+ This method probes the local environemnt and returns a (possibly empty) list |
+ of arguments to add to the Butler command line for authentication. |
+ |
+ If we're running on a GCE instance, no arguments will be returned, as GCE |
+ service account is implicitly authenticated. If we're running on Baremetal, |
+ a path to those credentials will be returned. |
+ |
+ Args: |
+ env (Environment): The execution environment. |
+ Raises: |
+ |LogDogBootstrapError| if no credentials could be found. |
+ """ |
+ path = env.get('service_account_json') |
+ if path: |
+ return path |
+ |
+ if GceAuthenticator.is_gce(): |
+ LOGGER.info('Running on GCE. No credentials necessary.') |
+ return None |
+ |
+ for credential_path in env.get('credential_paths', ()): |
+ candidate = os.path.join(credential_path, 'logdog_service_account.json') |
+ if os.path.isfile(candidate): |
+ return candidate |
+ |
+ raise LogDogBootstrapError('Could not find service account credentials.') |
+ |
+ |
+def _logdog_bootstrap(env, opts, cmd): |
+ butler, annotee = opts.logdog_butler_path, opts.logdog_annotee_path |
+ if not (butler and annotee): |
+ # Load packages via CIPD. |
+ cipd = CIPD.find(env) |
+ if env.cipd_packages: |
+ if not cipd: |
+ raise LogDogBootstrapError('Could not find CIPD binary.') |
+ cipd.ensure(**env.cipd_packages) |
+ if not butler: |
+ butler = cipd.path('logdog_butler') |
+ if not annotee: |
+ annotee = cipd.path('logdog_annotee') |
+ |
+ # Determine LogDog verbosity. |
+ if opts.logdog_verbose == 0: |
+ logdog_verbosity = 'warning' |
+ elif opts.logdog_verbose == 1: |
+ logdog_verbosity = 'info' |
+ else: |
+ logdog_verbosity = 'debug' |
+ |
+ service_account_args = [] |
+ service_account_json = _get_service_account_json(env) |
+ if service_account_json: |
+ service_account_args += ['-service-account-json', service_account_json] |
+ |
+ cmd = [ |
+ _assert_executable(butler), |
+ '-log_level', logdog_verbosity, |
+ ] + service_account_args + [ |
+ '-output', 'gcps,project="%s",topic="%s"' % (env.pubsub.project, |
+ env.pubsub.topic), |
+ 'run', |
+ '-streamserver-uri', env.butler_streamserver_uri(), |
+ '--', |
+ _assert_executable(annotee), |
+ '-log_level', logdog_verbosity, |
+ '--' |
+ ] + cmd |
+ rv, _ = _run_command(cmd, dry_run=opts.dry_run) |
+ if rv in LOGDOG_ERROR_RETURNCODES: |
+ raise LogDogBootstrapError('LogDog Error (%d)' % (rv,)) |
+ return rv |
+ |
+ |
+def _parse_args(args): |
+ parser = argparse.ArgumentParser() |
+ parser.add_argument('-v', '--verbose', |
+ action='count', default=0, |
+ help='Increase verbosity. This can be specified multiple times.') |
+ parser.add_argument('-V', '--logdog-verbose', |
+ action='count', default=0, |
+ help='Increase LogDog verbosity. This can be specified multiple times.') |
+ parser.add_argument('-d', '--dry-run', action='store_true', |
+ help='Perform configuration, but refrain from actually executing the ' |
+ 'command.') |
+ parser.add_argument('-f', '--force', action='store_true', |
+ help='Force LogDog bootstrapping, even if the system is not configured.') |
+ parser.add_argument('--logdog-butler-path', |
+ help='Path to the LogDog Butler. If empty, one will be probed/downloaded ' |
+ 'from CIPD.') |
+ parser.add_argument('--logdog-annotee-path', |
+ help='Path to the LogDog Annotee. If empty, one will be ' |
+ 'probed/downloaded from CIPD.') |
+ parser.add_argument('--service-account-json', |
+ help='Path to the service account JSON. If one is not provided, the ' |
+ 'local system credentials will be used.') |
+ parser.add_argument('args', nargs=argparse.REMAINDER) |
+ |
+ opts, extras = parser.parse_known_args(args) |
+ if opts.args and opts.args[0] == '--': |
+ opts.args.pop(0) |
+ if extras: |
+ opts.args = extras + opts.args |
+ return opts |
+ |
+ |
+def main(args): |
+ opts = _parse_args(args) |
+ cmd = [ANNOTATED_RUN] + opts.args |
+ |
+ if opts.verbose == 0: |
+ level = logging.WARNING |
+ elif opts.verbose == 1: |
+ level = logging.INFO |
+ else: |
+ level = logging.DEBUG |
+ logging.getLogger().setLevel(level) |
+ |
+ ran_bootstrap = False |
+ rv = 1 |
+ try: |
+ with Environment.probe( |
+ os.environ, |
+ service_account_json=opts.service_account_json) as env: |
+ LOGGER.debug('Loaded environment: %s', env) |
+ if not opts.force: |
+ env.assert_whitelisted() |
+ |
+ rv = _logdog_bootstrap(env, opts, cmd) |
+ ran_bootstrap = True |
+ except LogDogNotBootstrapped as e: |
+ LOGGER.info('Not bootstrapped: %s', e.message) |
+ except LogDogBootstrapError as e: |
+ LOGGER.warning('Could not bootstrap LogDog: %s', e.message) |
+ except Exception: |
+ LOGGER.exception('Exception while bootstrapping LogDog.') |
+ finally: |
+ if not ran_bootstrap: |
+ LOGGER.info('Not using LogDog. Invoking `annotated_run.py` directly.') |
+ rv, _ = _run_command(cmd, opts) |
+ return rv |
+ |
+ |
+if __name__ == '__main__': |
+ logging.basicConfig() |
+ sys.exit(main(sys.argv[1:])) |