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

Unified Diff: scripts/slave/annotated_run.py

Issue 1468053008: Add LogDog bootstrapping to `annotated_run.py`. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/build
Patch Set: Fixes? Created 5 years 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 | « no previous file | scripts/slave/gce.py » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: scripts/slave/annotated_run.py
diff --git a/scripts/slave/annotated_run.py b/scripts/slave/annotated_run.py
index 724ed9d02a068e0fae2362f7a642db1f3fc61901..174bfd373dd85ce13cb00ffb24a669dbd1cc353d 100755
--- a/scripts/slave/annotated_run.py
+++ b/scripts/slave/annotated_run.py
@@ -26,10 +26,31 @@ from common import annotator
from common import chromium_utils
from common import env
from common import master_cfg_utils
+from slave import gce
# Logging instance.
LOGGER = logging.getLogger('annotated_run')
+# 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 of {master}=>[{builder}|WHITELIST_ALL] whitelisting specific masters
+# and builders for experimental LogDog/Annotee export.
+LOGDOG_WHITELIST_MASTER_BUILDERS = {
+}
+
+# Sentinel value that, if present in master config, matches all builders
+# underneath that master.
+WHITELIST_ALL = '*'
+
+# Configuration for a Pub/Sub topic.
+PubSubConfig = collections.namedtuple('PubSubConfig', ('project', 'topic'))
# RecipeRuntime will probe this for values.
# - First, (system, platform)
@@ -37,11 +58,21 @@ LOGGER = logging.getLogger('annotated_run')
# - Finally, (),
PLATFORM_CONFIG = {
# All systems.
- (): {},
+ (): {
+ 'logdog_pubsub': PubSubConfig(
+ project='luci-logdog',
+ topic='chrome-infra-beta',
+ ),
+ },
# Linux
('Linux',): {
'run_cmd': ['/opt/infra-python/run.py'],
+ 'credential_paths': (
+ # XXX: Get this right?
+ '/opt/infra/service_accounts',
+ ),
+ 'logdog_butler_streamserver_gen': lambda d: os.path.join(d, 'butler.sock'),
},
# Mac OSX
@@ -61,6 +92,9 @@ PLATFORM_CONFIG = {
# the recipe engine.
Config = collections.namedtuple('Config', (
'run_cmd',
+ 'logdog_pubsub',
+ 'logdog_butler_streamserver_gen',
+ 'credential_paths',
))
@@ -83,6 +117,10 @@ def get_config():
# Construct runtime configuration.
return Config(
run_cmd=platform_config.get('run_cmd'),
+ logdog_pubsub=platform_config.get('logdog_pubsub'),
+ logdog_butler_streamserver_gen=platform_config.get(
+ 'logdog_butler_streamserver_gen'),
+ credential_paths=platform_config.get('credential_paths', ()),
)
@@ -144,6 +182,256 @@ def recipe_tempdir(root=None, leak=False):
LOGGER.warning('(--leak) Leaking temporary directory [%s].', basedir)
+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 ensure_directory(*path):
+ path = os.path.join(*path)
+ if not os.path.isdir(path):
+ os.makedirs(path)
+ return path
+
+
+def _run_command(cmd, **kwargs):
+ dry_run = kwargs.pop('dry_run', False)
+
+ LOGGER.debug('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] returned [%d] with output:\n%s',
+ cmd, proc.returncode, 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 RecipeRuntime(object):
+ """RecipeRuntime is the platform-specific runtime enviornment.
+
+ The runtime is loaded with a set of read-only attributes that are a
+ combination of plaetform and runtime values used in the setup and execution of
+ the recipe engine.
+ """
+
+ _SENTINEL = object()
+
+ def __init__(self, **kwargs):
+ self._attrs = kwargs
+
+ @classmethod
+ @contextlib.contextmanager
+ def enter(cls, leak, **kw):
+ """Enters the annotated_run environment.
+
+ This creates a temporary directory for this annotation run that is
+ automatically cleaned up. It returns a RecipeRuntime object containing a
+ combination of the supplied keyword arguments and the platform-specific
+ configuration.
+
+ Args:
+ leak (bool): If true, don't clean up the temporary directory on exit.
+ kw (dict): Key/value pairs to add as attributes to the RecipeRuntime.
+ """
+ # Build our platform attributes.
+ p = (platform.system(), platform.processor())
+ attrs = {}
+ for i in xrange(len(p)+1):
+ attrs.update(PLATFORM_CONFIG.get(p[:i], {}))
+ attrs.update(kw)
+
+ basedir = ensure_directory(os.getcwd(), '.recipe_runtime')
+ try:
+ tdir = tempfile.mkdtemp(dir=basedir)
+ LOGGER.debug('Using temporary directory [%s].', tdir)
+
+ attrs['workdir'] = tdir
+ yield cls(**attrs)
+ finally:
+ if basedir and os.path.isdir(basedir):
+ if not leak:
+ LOGGER.debug('Cleaning up temporary directory [%s].', basedir)
+ try:
+ # TODO(pgervais): use infra_libs.rmtree instead.
+ shutil.rmtree(basedir)
+ except Exception:
+ LOGGER.exception('Failed to clean up temporary directory [%s].',
+ basedir)
+ else:
+ LOGGER.warning('(--leak) Leaking temporary directory [%s].', basedir)
+
+ def __getattr__(self, key):
+ # Class methods/variables.
+ value = getattr(super(RecipeRuntime, self), key, self._SENTINEL)
+ if value is not self._SENTINEL:
+ return value
+
+ value = getattr(self, 'get')(key, self._SENTINEL)
+ if value is not self._SENTINEL:
+ return value
+ raise KeyError(key)
+
+ def get(self, key, default=None):
+ value = self._attrs.get(key, self._SENTINEL)
+ if value is not self._SENTINEL:
+ return value
+ return default
+
+ def __str__(self):
+ return str(self._attrs)
+
+
+def _get_service_account_json(opts, credential_paths):
+ """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:
+ rt (RecipeRuntime): The runtime environment.
+ Raises:
+ |LogDogBootstrapError| if no credentials could be found.
+ """
+ path = opts.get('service_account_json')
+ if path:
+ return path
+
+ if gce.Authenticator.is_gce():
+ LOGGER.info('Running on GCE. No credentials necessary.')
+ return None
+
+ for credential_path in 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. '
+ 'Tried: %s' % (credential_paths,))
+
+
+def _logdog_bootstrap(tempdir, config, opts, cmd):
+ """Executes the recipe engine, bootstrapping it through LogDog/Annotee.
+
+ This method executes the recipe engine, bootstrapping it through
+ LogDog/Annotee so its output and annotations are streamed to LogDog. The
+ bootstrap is configured to tee the annotations through STDOUT/STDERR so they
+ will still be sent to BuildBot.
+
+ The overall setup here is:
+ [annotated_run.py] => [logdog_butler] => [logdog_annotee] => [recipes.py]
+
+ Args:
+ config (Config): Recipe runtime configuration.
+ opts (argparse.Namespace): Command-line options.
+ cmd (list): The recipe runner command list to bootstrap.
+
+ Returns (int): The return code of the recipe runner process.
+
+ Raises:
+ LogDogNotBootstrapped: if the recipe engine was not executed because the
+ LogDog bootstrap requirements are not available.
+ LogDogBootstrapError: if there was an error bootstrapping the recipe runner
+ through LogDog.
+ """
+ bootstrap_dir = ensure_directory(tempdir, 'logdog_bootstrap')
+ butler, annotee = opts.logdog_butler_path, opts.logdog_annotee_path
+
+ if not is_executable(annotee):
+ raise LogDogNotBootstrapped('Annotee is not executable: %s' % (annotee,))
+ if not is_executable(butler):
+ raise LogDogNotBootstrapped('Butler is not executable: %s' % (butler,))
+
+ # Determine LogDog verbosity.
+ logdog_verbose = []
+ if opts.logdog_verbose == 0:
+ pass
+ elif opts.logdog_verbose == 1:
+ logdog_verbose.extend('-log_level=info')
+ else:
+ logdog_verbose.extend('-log_level=debug')
+
+ service_account_args = []
+ service_account_json = _get_service_account_json(
+ opts, config.credential_paths)
+ if service_account_json:
+ service_account_args += ['-service-account-json', service_account_json]
+
+ streamserver_uri_gen = config.logdog_butler_streamserver_gen
+ if not streamserver_uri_gen:
+ raise LogDogBootstrapError('No streamserver URI generator.')
+ streamserver_uri = streamserver_uri_gen(tempdir)
+
+ # Dump Annotee command to JSON.
+ cmd_json = os.path.join(bootstrap_dir, 'annotee_cmd.json')
+ with open(cmd_json, 'w') as fd:
+ json.dump(cmd, fd)
+
+ cmd = [
+ # Butler Command.
+ butler,
+ ] + logdog_verbose + service_account_args + [
+ '-output', 'gcps,project="%s",topic="%s"' % (config.logdog_pubsub.project,
+ config.logdog_pubsub.topic),
+ 'run',
+ '-streamserver-uri', streamserver_uri,
+ '--',
+
+ # Annotee Command.
+ annotee,
+ ] + logdog_verbose + [
+ '-json-args-path', cmd_json,
+ ]
+ rv, _ = _run_command(cmd, dry_run=opts.dry_run)
+ if rv in LOGDOG_ERROR_RETURNCODES:
+ raise LogDogBootstrapError('LogDog Error (%d)' % (rv,))
+ return rv
+
+
+def _assert_logdog_whitelisted(mastername, buildername):
+ """Asserts that the runtime environment is whitelisted for LogDog bootstrap.
+
+ Args:
+ mastername (str): The master name string.
+ buildername (str): The builder name.
+ Raises:
+ LogDogNotBootstrapped: if the runtime is not whitelisted.
+ """
+ if not all((mastername, buildername)):
+ raise LogDogNotBootstrapped('Required mastername/buildername is not set.')
+
+ # Key on mastername.
+ bdict = LOGDOG_WHITELIST_MASTER_BUILDERS.get(mastername)
+ if bdict is not None:
+ # Key on buildername.
+ if WHITELIST_ALL in bdict or buildername in bdict:
+ LOGGER.info('Whitelisted master %s, builder %s.',
+ mastername, buildername)
+ return
+ raise LogDogNotBootstrapped('Master %s, builder %s is not whitelisted.' % (
+ mastername, buildername))
+
+
def get_recipe_properties(workdir, build_properties,
use_factory_properties_from_disk):
"""Constructs the recipe's properties from buildbot's properties.
@@ -291,6 +579,22 @@ def get_args(argv):
action='store_true', default=False,
help='use factory properties loaded from disk on the slave')
+ group = parser.add_argument_group('LogDog Bootstrap')
+ group.add_argument('-V', '--logdog-verbose',
+ action='count', default=0,
+ help='Increase LogDog verbosity. This can be specified multiple times.')
+ group.add_argument('-f', '--logdog-force', action='store_true',
+ help='Force LogDog bootstrapping, even if the system is not configured.')
+ group.add_argument('--logdog-butler-path',
+ help='Path to the LogDog Butler. If empty, one will be probed/downloaded '
+ 'from CIPD.')
+ group.add_argument('--logdog-annotee-path',
+ help='Path to the LogDog Annotee. If empty, one will be '
+ 'probed/downloaded from CIPD.')
+ group.add_argument('--logdog-service-account-json',
+ help='Path to the service account JSON. If one is not provided, the '
+ 'local system credentials will be used.')
+
return parser.parse_args(argv)
@@ -477,7 +781,21 @@ def main(argv):
properties['recipe'],
]
- status, _ = _run_command(cmd, dry_run=opts.dry_run)
+ status = None
+ try:
+ if not opts.logdog_force:
+ _assert_logdog_whitelisted(config.mastername, config.buildername)
+ status = _logdog_bootstrap(tdir, config, opts, cmd)
+ 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 status is None:
+ LOGGER.info('Not using LogDog. Invoking `annotated_run.py` directly.')
+ status, _ = _run_command(cmd, dry_run=opts.dry_run)
return status
« no previous file with comments | « no previous file | scripts/slave/gce.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698