blob: b85911fb9dc4fa32c4192a20ad0bcdf0fabde209 [file] [log] [blame]
#!/usr/bin/env python
# Copyright 2016 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 copy
import itertools
import json
import logging
import os
import subprocess
import sys
import tempfile
import urllib
# Install Infra build environment.
BUILD_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(
sys.path.insert(0, os.path.join(BUILD_ROOT, 'scripts'))
# calling common.env.Install() globally causes annotated_run recipes to
# crash because it installs the system python lib path(s) to PYTHONPATH,
# which don't mix well with the bundled python we use. Doing it this way
# will allow us to load what we need, without polluting PYTHONPATH permanently.
import common.env
with common.env.GetInfraPythonPath().Enter():
import httplib2
from oauth2client import gce
from oauth2client.client import GoogleCredentials
from common import annotator
from common import chromium_utils
from slave import cipd
from slave import infra_platform
from slave import logdog_bootstrap
from slave import monitoring_utils
from slave import robust_tempdir
from slave import update_scripts
# BuildBot root directory: /b
BUILDBOT_ROOT = os.path.abspath(os.path.dirname(BUILD_ROOT))
LOGGER = logging.getLogger('remote_run')
# Masters that are running "canary" run.
# Volunteered by bpastene@ as a generically representative waterfall that is
# not a big deal if it breaks.
# The name of the recipe engine CIPD package.
_RECIPES_PY_CIPD_PACKAGE = 'infra/recipes-py'
# _CIPD_PINS is a mapping of master name to pinned CIPD package version to use
# for that master.
CipdPins = collections.namedtuple('CipdPins', ('recipes',))
# Stable CIPD pin set.
# Canary CIPD pin set.
def _ensure_directory(*path):
path = os.path.join(*path)
if not os.path.isdir(path):
return path
def _try_cleanup(src, cleanup_dir):
"""Stages the "src" file or directory for removal via "cleanup_dir".
The "cleanup_dir" is a BuildBot-provided facility that deletes files
in between builds. Moving files into this directory completes instantly. As
opposed to deleting in "remote_run" or a lower layer, deletions via
"cleanup_dir" happen in between builds, meaning that the overhead and
expense aren't subject to I/O timeout and don't affect actual build times.
NOTE: We rely on "cleanup_dir" to be on the same filesystem as the source
directories, which is the case for BuildBot builds.
if not os.path.isdir(cleanup_dir):
LOGGER.warning('Cleanup directory does not exist: %r', cleanup_dir)
base = os.path.basename(src)
target_dir = tempfile.mkdtemp(prefix=base, dir=cleanup_dir)
dst = os.path.join(target_dir, base)'Moving file to cleanup directory %r => %r', src, dst)
os.rename(src, dst)
except Exception:
LOGGER.exception('Failed to cleanup path %r', src)
def _get_is_canary(mastername):
return mastername in _CANARY_MASTERS
def find_python():
if sys.platform == 'win32':
candidates = ['python.exe', 'python.bat']
candidates = ['python']
path_env = os.environ.get('PATH', '')
for base in path_env.split(os.pathsep):
for c in candidates:
path = os.path.join(base, c)
if os.path.isfile(path) and os.access(path, os.F_OK|os.X_OK):
return os.path.abspath(path)
LOGGER.warning('Could not find Python in PATH: %r', path_env)
return sys.executable
def all_cipd_manifests():
"""Generator which yields all CIPD ensure manifests (canary, staging, prod).
Each manifest is represented by a list of cipd.CipdPackage instances.
# All CIPD packages are in top-level platform config.
yield [
def set_recipe_runtime_properties(stream, args, properties):
"""Looks at mastername/buildername in $properties and contacts
`` to see if LUCI is prod for this builder yet.
* stream - an StructuredAnnotationStream that this function will use to
present a 'LUCI Migration' step (and display generated migration
* args - the optparse/argparse result object containing the logdog_bootstrap
* properties (in/out dictionary) - the recipe properties.
Modifies `properties` to contain `$recipe_engine/runtime`.
ret = {
'is_experimental': False,
'is_luci': False,
migration = {'status': 'error'}
cred_path = logdog_bootstrap.get_config(
args, properties).service_account_path
master = properties['mastername']
builder = properties['buildername']
# piggyback on logdog's service account; this needs to run everywhere that
# logdog does, and since it's migration-buildbot-only code, this seems like
# a reasonable compromise to make this rollout as quick as possible.
scopes = ['']
if cred_path == logdog_bootstrap.GCE_CREDENTIALS:
cred = gce.AppAssertionCredentials(scopes)
cred = GoogleCredentials.from_stream(cred_path).create_scoped(scopes)
http = httplib2.Http()
url = (''
resp, body = http.request(
url % (urllib.quote(master), urllib.quote(builder)))
if resp.status == 200:
ret['is_experimental'] = json.loads(body)['luci_is_prod']
migration['status'] = 'ok'
migration['status'] = 'bad_status'
migration['code'] = resp.status
except Exception as ex:
migration['error'] = str(ex)
properties['$recipe_engine/runtime'] = ret
properties['luci_migration'] = migration
with stream.step('LUCI Migration') as st:
st.set_build_property('$recipe_engine/runtime', json.dumps(ret))
st.set_build_property('luci_migration', json.dumps(migration))
def _call(cmd, **kwargs):'Executing command: %s', cmd)
exit_code =, **kwargs)'Command %s finished with exit code %d.', cmd, exit_code)
return exit_code
def _cleanup_old_layouts(buildbot_build_dir, cleanup_dir, cache_dir,
properties = properties or {}
cleanup_paths = [
# All remote_run instances no longer use "//build/slave/cache_dir" or
# "//build/slave/goma_cache", preferring to use cache directories
# instead (see "infra_paths" recipe module).
os.path.join(BUILD_ROOT, 'slave', 'cache_dir'),
os.path.join(BUILD_ROOT, 'slave', 'goma_cache'),
# "bot_update" uses "build.dead" as a way to automatically purge
# directories. While this is being handled differently, existing
# "build.dead" directories currently exist under "[CACHE]/b/build.dead"
# due to the previous logic.
os.path.join(cache_dir, 'b', 'build.dead'),
# Make switching to remote_run easier: we do not use buildbot workdir,
# and it takes disk space leading to out of disk errors.
buildbot_workdir = properties.get('workdir')
if buildbot_workdir and os.path.isdir(buildbot_workdir):
buildbot_workdir = os.path.realpath(buildbot_workdir)
buildbot_build_dir = os.path.realpath(buildbot_build_dir)
if buildbot_build_dir.startswith(buildbot_workdir):
buildbot_workdir = buildbot_build_dir
# Buildbot workdir is usually used as current working directory,
# so do not remove it, but delete all of the contents. Deleting
# current working directory of a running process may cause
# confusing errors.
cleanup_paths.extend(os.path.join(buildbot_workdir, x)
for x in os.listdir(buildbot_workdir))
except Exception:
# It's preferred that we keep going rather than fail the build
# on optional cleanup.
LOGGER.exception('Buildbot workdir cleanup failed: %s', buildbot_workdir)
# If we have a 'git' cache directory from a previous Kitchen run, we
# should delete that in favor of the 'git_cache' cache directory.
cleanup_paths.append(os.path.join(cache_dir, 'git'))
# We want to delete "<cache>/builder" from the Kitchen run.
cleanup_paths.append(os.path.join(cache_dir, 'builder'))
cleanup_paths = [p for p in cleanup_paths if os.path.exists(p)]
if cleanup_paths:'Cleaning up %d old layout path(s)...', len(cleanup_paths))
# We remove files by moving them to the cleanup directory. This causes them
# to be deleted in between builds.
for path in cleanup_paths:'Removing path from previous layout: %r', path)
_try_cleanup(path, cleanup_dir)
def _exec_recipe(args, rt, stream, basedir, buildbot_build_dir, cleanup_dir,
tempdir = rt.tempdir(basedir)'Using temporary directory: %r.', tempdir)
build_data_dir = rt.tempdir(basedir)'Using build data directory: %r.', build_data_dir)
# Construct our properties.
properties = copy.copy(args.factory_properties)
# Determine our CIPD pins.
# If a property includes "remote_run_canary", we will explicitly use canary
# pins. This can be done by manually submitting a build to the waterfall.
mastername = properties.get('mastername')
buildername = properties.get('buildername')
is_canary = (_get_is_canary(mastername) or
'remote_run_canary' in properties or args.canary)
pins = _STABLE_CIPD_PINS if not is_canary else _CANARY_CIPD_PINS
# Augment our input properties...
def set_property(key, value):
properties[key] = value
print '@@@SET_BUILD_PROPERTY@%s@%s@@@' % (key, json.dumps(value))
set_property('build_data_dir', build_data_dir)
set_property('builder_id', 'master.%s:%s' % (mastername, buildername))
if 'buildnumber' in properties:
# path_config property defines what paths a build uses for checkout, git
# cache, goma cache, etc.
# TODO(dnj or phajdan): Rename "kitchen" path config to "remote_run_legacy".
# "kitchen" was never correct, and incorrectly implies that Kitchen is
# somehow involved int his path config.
properties['path_config'] = 'kitchen'
properties['bot_id'] = properties['slavename']
# Set our cleanup directory to be "build.dead" so that BuildBot manages it.
properties['$recipe_engine/path'] = {
'cleanup_dir': cleanup_dir,
set_recipe_runtime_properties(stream, args, properties)
monitoring_utils.write_build_monitoring_event(build_data_dir, properties)
# Ensure that the CIPD client and base tooling is installed and available on
from slave import cipd_bootstrap_v2
track = cipd_bootstrap_v2.PROD
prefix_paths = cipd_bootstrap_v2.high_level_ensure_cipd_client(
basedir, mastername, track=track)
properties['$recipe_engine/step'] = {'prefix_path': prefix_paths}'Using properties: %r', properties)
# Cleanup data from old builds.
buildbot_build_dir, cleanup_dir, cache_dir,
properties_file = os.path.join(tempdir, 'remote_run_properties.json')
with open(properties_file, 'w') as f:
json.dump(properties, f)
cipd_path = os.path.join(basedir, '.remote_run_cipd')
engine_args = []
recipe_result_path = os.path.join(tempdir, 'recipe_result.json')
recipe_cmd = [
os.path.join(cipd_path, ''),] + engine_args + [
'--repository', args.repository,
'--workdir', os.path.join(tempdir, 'rw'),
if args.revision:
recipe_cmd.extend(['--revision', args.revision])
'--',] + (
engine_args) + [
'--properties-file', properties_file,
'--workdir', os.path.join(tempdir, 'w'),
'--output-result-json', recipe_result_path,
properties.get('recipe') or args.recipe,
# If we bootstrap through logdog, the recipe command line gets written
# to a temporary file and does not appear in the log.'Recipe command line: %r', recipe_cmd)
environ = os.environ.copy()
# Default to return code != 0 is for the benefit of buildbot, which uses
# return code to decide if a step failed or not.
recipe_return_code = 1
bs = logdog_bootstrap.bootstrap(rt, args, basedir, tempdir, properties,
recipe_cmd)'Bootstrapping through LogDog: %s', bs.cmd)
_ = _call(bs.cmd, env=environ)
recipe_return_code = bs.get_result()
except logdog_bootstrap.NotBootstrapped:'Not using LogDog. Invoking `` directly.')
recipe_return_code = _call(recipe_cmd, env=environ)
# Try to open recipe result JSON. Any failure will result in an exception
# and an infra failure.
with open(recipe_result_path) as f:
return_value = json.load(f)
f = return_value.get('failure')
if f is not None and not f.get('step_failure'):
# The recipe engine used to return -1, which got interpreted as 255
# by os.exit in python, since process exit codes are a single byte.
recipe_return_code = 255
return recipe_return_code
def _main_impl(argv, stream):
parser = argparse.ArgumentParser()
parser.add_argument('--repository', required=True,
help='URL of a git repository to fetch.')
help='Git commit hash to check out.')
parser.add_argument('--recipe', required=True,
help='Name of the recipe to run')
parser.add_argument('--build-properties-gz', dest='build_properties',
type=chromium_utils.convert_gz_json_type, default={},
help='Build properties in b64 gz JSON format')
parser.add_argument('--factory-properties-gz', dest='factory_properties',
type=chromium_utils.convert_gz_json_type, default={},
help='factory properties in b64 gz JSON format')
parser.add_argument('--leak', action='store_true',
help='Refrain from cleaning up generated artifacts.')
parser.add_argument('--canary', action='store_true',
help='Force use of canary configuration.')
parser.add_argument('--verbose', action='store_true')
group = parser.add_argument_group('LogDog Bootstrap')
args = parser.parse_args(argv[1:])
buildbot_build_dir = os.path.abspath(os.getcwd())
basedir = chromium_utils.FindUpward(buildbot_build_dir, 'b')
except chromium_utils.PathNotFound as e:
# Use base directory inside system temporary directory - if we use slave
# one (cwd), the paths get too long. Recipes which need different paths
# or persistent directories should do so explicitly.
basedir = tempfile.gettempdir()
# "/b/c" as a cache directory.
cache_dir = os.path.join(BUILDBOT_ROOT, 'c')
# BuildBot automatically purges "build.dead", and recipe engine uses this as
# its cleanup directory (see "infra_paths" recipe module). Make sure that it
# exists, and retain it so that we can use it to perform "annotated_run" to
# "remote_run" path cleanup.
cleanup_dir = os.path.join(
os.path.dirname(buildbot_build_dir), 'build.dead')
# Cleanup system and temporary directories.
from slave import cleanup_temp
# Note that this will delete "cleanup_dir", so we will need to
# recreate it afterwards.
except cleanup_temp.FullDriveException:
LOGGER.error('Buildslave disk is full! Please contact the trooper.')
# Our cleanup failed because the disk is full! Do a best-effort cleanup in
# hopes that the next run, we can get farther than this.
_cleanup_old_layouts(buildbot_build_dir, cleanup_dir, cache_dir)
# Ensure that "cleanup_dir" exists; our recipes expect this to be the case.
# Choose a tempdir prefix. If we have no active subdir, we will use a prefix
# of "rr". If we have an active subdir, we will use "rs/<subdir>". This way,
# there will be no tempdir collisions between combinations of the two
# sitautions.
active_subdir = chromium_utils.GetActiveSubdir()
if active_subdir:
prefix = os.path.join('rs', str(active_subdir))
prefix = 'rr'
with robust_tempdir.RobustTempdir(prefix, leak=args.leak) as rt:
# Explicitly clean up possibly leaked temporary directories
# from previous runs.
return _exec_recipe(args, rt, stream, basedir, buildbot_build_dir,
cleanup_dir, cache_dir)
def main(argv, stream, passthrough=False):
# We always want everything to be unbuffered so that we can see the logs on
# buildbot/logdog as soon as they're available.
os.environ['PYTHONUNBUFFERED'] = '1'
exc_info = None
return _main_impl(argv, stream)
except Exception:
exc_info = sys.exc_info()
# Report on the "remote_run" execution. If an exception (infra failure)
# occurred, raise it so that the build and the step turn purple.
with stream.step('remote_run_result') as s:
if passthrough:
if exc_info is not None:
raise exc_info[0], exc_info[1], exc_info[2]
def shell_main(argv):
level=(logging.DEBUG if '--verbose' in argv else logging.INFO))
if update_scripts.update_scripts():
# Re-execute with the updated
return _call([sys.executable] + argv)
stream = annotator.StructuredAnnotationStream()
return main(argv, stream)
if __name__ == '__main__':