blob: 9fb8b581c79d476714f7ef7e62590e011b25b572 [file] [log] [blame]
# Copyright 2015 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Configuration options for various cbuildbot builders."""
from __future__ import print_function
import copy
import json
from chromite.cbuildbot import constants
from chromite.lib import osutils
GS_PATH_DEFAULT = 'default' # Means gs://chromeos-image-archive/ + bot_id
# Contains the valid build config suffixes in the order that they are dumped.
CONFIG_TYPE_PRECQ = 'pre-cq'
CONFIG_TYPE_PALADIN = 'paladin'
CONFIG_TYPE_RELEASE = 'release'
CONFIG_TYPE_FULL = 'full'
CONFIG_TYPE_FIRMWARE = 'firmware'
CONFIG_TYPE_FACTORY = 'factory'
CONFIG_TYPE_RELEASE_AFDO = 'release-afdo'
# This is only used for unitests... find a better solution?
CONFIG_TYPE_DUMP_ORDER = (
CONFIG_TYPE_PALADIN,
constants.PRE_CQ_GROUP_CONFIG,
CONFIG_TYPE_PRECQ,
constants.PRE_CQ_LAUNCHER_CONFIG,
'incremental',
'telemetry',
CONFIG_TYPE_FULL,
'full-group',
CONFIG_TYPE_RELEASE,
'release-group',
'release-afdo',
'release-afdo-generate',
'release-afdo-use',
'sdk',
'chromium-pfq',
'chromium-pfq-informational',
'chrome-perf',
'chrome-pfq',
'chrome-pfq-informational',
'pre-flight-branch',
CONFIG_TYPE_FACTORY,
CONFIG_TYPE_FIRMWARE,
'toolchain-major',
'toolchain-minor',
'asan',
'asan-informational',
'refresh-packages',
'test-ap',
'test-ap-group',
constants.BRANCH_UTIL_CONFIG,
constants.PAYLOADS_TYPE,
)
# In the Json, this special build config holds the default values for all
# other configs.
DEFAULT_BUILD_CONFIG = '_default'
def IsPFQType(b_type):
"""Returns True if this build type is a PFQ."""
return b_type in (constants.PFQ_TYPE, constants.PALADIN_TYPE,
constants.CHROME_PFQ_TYPE)
def IsCQType(b_type):
"""Returns True if this build type is a Commit Queue."""
return b_type == constants.PALADIN_TYPE
def IsCanaryType(b_type):
"""Returns True if this build type is a Canary."""
return b_type == constants.CANARY_TYPE
class BuildConfig(dict):
"""Dictionary of explicit configuration settings for a cbuildbot config
Each dictionary entry is in turn a dictionary of config_param->value.
See _settings for details on known configurations, and their documentation.
"""
_delete_key_sentinel = object()
@classmethod
def delete_key(cls):
"""Used to remove the given key from inherited config.
Usage:
new_config = base_config.derive(foo=delete_key())
"""
return cls._delete_key_sentinel
@classmethod
def delete_keys(cls, keys):
"""Used to remove a set of keys from inherited config.
Usage:
new_config = base_config.derive(delete_keys(set_of_keys))
"""
return {k: cls._delete_key_sentinel for k in keys}
def __getattr__(self, name):
"""Support attribute-like access to each dict entry."""
if name in self:
return self[name]
# Super class (dict) has no __getattr__ method, so use __getattribute__.
return super(BuildConfig, self).__getattribute__(name)
def GetBotId(self, remote_trybot=False):
"""Get the 'bot id' of a particular bot.
The bot id is used to specify the subdirectory where artifacts are stored
in Google Storage. To avoid conflicts between remote trybots and regular
bots, we add a 'trybot-' prefix to any remote trybot runs.
Args:
remote_trybot: Whether this run is a remote trybot run.
"""
return 'trybot-%s' % self.name if remote_trybot else self.name
def deepcopy(self):
"""Create a deep copy of this object.
This is a specialized version of copy.deepcopy() for BuildConfig objects. It
speeds up deep copies by 10x because we know in advance what is stored
inside a BuildConfig object and don't have to do as much introspection. This
function is called a lot during setup of the config objects so optimizing it
makes a big difference. (It saves seconds off the load time of this module!)
"""
new_config = BuildConfig(self)
for k, v in self.iteritems():
# type(v) is faster than isinstance.
if type(v) is list:
new_config[k] = v[:]
if new_config.get('child_configs'):
new_config['child_configs'] = [
x.deepcopy() for x in new_config['child_configs']]
if new_config.get('hw_tests'):
new_config['hw_tests'] = [copy.copy(x) for x in new_config['hw_tests']]
return new_config
def derive(self, *args, **kwargs):
"""Create a new config derived from this one.
Note: If an override is callable, it will be called and passed the prior
value for the given key (or None) to compute the new value.
Args:
args: Mapping instances to mixin.
kwargs: Settings to inject; see _settings for valid values.
Returns:
A new _config instance.
"""
inherits = list(args)
inherits.append(kwargs)
new_config = self.deepcopy()
for update_config in inherits:
for k, v in update_config.iteritems():
if callable(v):
new_config[k] = v(new_config.get(k))
else:
new_config[k] = v
keys_to_delete = [k for k in new_config if
new_config[k] is self._delete_key_sentinel]
for k in keys_to_delete:
new_config.pop(k, None)
return new_config
def HideDefaults(self, default):
"""Create a duplicate BuildConfig with default values missing.
Args:
default: Dictionary of key/value pairs to remove, if present.
Returns:
A copy BuildConfig, that only contains values different from defaults.
"""
d = BuildConfig()
for k, v in self.iteritems():
# Recurse to children.
if k == 'child_configs':
v = [child.HideDefaults(default) for child in v]
# Only store a value if it's different from default.
if k not in default or default[k] != v:
d[k] = v
return d
class HWTestConfig(object):
"""Config object for hardware tests suites.
Members:
suite: Name of the test suite to run.
timeout: Number of seconds to wait before timing out waiting for
results.
pool: Pool to use for hw testing.
blocking: Suites that set this true run sequentially; each must pass
before the next begins. Tests that set this false run in
parallel after all blocking tests have passed.
async: Fire-and-forget suite.
warn_only: Failure on HW tests warns only (does not generate error).
critical: Usually we consider structural failures here as OK.
priority: Priority at which tests in the suite will be scheduled in
the hw lab.
file_bugs: Should we file bugs if a test fails in a suite run.
num: Maximum number of DUTs to use when scheduling tests in the hw lab.
minimum_duts: minimum number of DUTs required for testing in the hw lab.
retry: Whether we should retry tests that fail in a suite run.
max_retries: Integer, maximum job retries allowed at suite level.
None for no max.
suite_min_duts: Preferred minimum duts. Lab will prioritize on getting such
number of duts even if the suite is competing with
other suites that have higher priority.
Some combinations of member settings are invalid:
* A suite config may not specify both blocking and async.
* A suite config may not specify both retry and async.
* A suite config may not specify both warn_only and critical.
"""
# This timeout is larger than it needs to be because of autotest overhead.
# TODO(davidjames): Reduce this timeout once http://crbug.com/366141 is fixed.
DEFAULT_HW_TEST_TIMEOUT = 60 * 220
BRANCHED_HW_TEST_TIMEOUT = 10 * 60 * 60
def __init__(self, suite, num=constants.HWTEST_DEFAULT_NUM,
pool=constants.HWTEST_MACH_POOL, timeout=DEFAULT_HW_TEST_TIMEOUT,
async=False, warn_only=False, critical=False, blocking=False,
file_bugs=False, priority=constants.HWTEST_BUILD_PRIORITY,
retry=True, max_retries=10, minimum_duts=0, suite_min_duts=0):
"""Constructor -- see members above."""
assert not async or (not blocking and not retry)
assert not warn_only or not critical
self.suite = suite
self.num = num
self.pool = pool
self.timeout = timeout
self.blocking = blocking
self.async = async
self.warn_only = warn_only
self.critical = critical
self.file_bugs = file_bugs
self.priority = priority
self.retry = retry
self.max_retries = max_retries
self.minimum_duts = minimum_duts
self.suite_min_duts = suite_min_duts
def SetBranchedValues(self):
"""Changes the HW Test timeout/priority values to branched values."""
self.timeout = max(HWTestConfig.BRANCHED_HW_TEST_TIMEOUT, self.timeout)
# Set minimum_duts default to 0, which means that lab will not check the
# number of available duts to meet the minimum requirement before creating
# a suite job for branched build.
self.minimum_duts = 0
# Only reduce priority if it's lower.
new_priority = constants.HWTEST_DEFAULT_PRIORITY
if (constants.HWTEST_PRIORITIES_MAP[self.priority] >
constants.HWTEST_PRIORITIES_MAP[new_priority]):
self.priority = new_priority
@property
def timeout_mins(self):
return int(self.timeout / 60)
def __eq__(self, other):
return self.__dict__ == other.__dict__
class SiteConfig(dict):
"""This holds a set of named BuildConfig values."""
def __init__(self, defaults=None):
"""Init."""
super(SiteConfig, self).__init__()
self._defaults = {} if defaults is None else defaults
def GetDefault(self):
"""Create the cannonical default build configuration."""
# Enumeration of valid settings; any/all config settings must be in this.
# All settings must be documented.
return BuildConfig(**self._defaults)
#
# Methods for searching a SiteConfig's contents.
#
def FindFullConfigsForBoard(self, board=None):
"""Returns full builder configs for a board.
Args:
board: The board to match. By default, match all boards.
Returns:
A tuple containing a list of matching external configs and a list of
matching internal release configs for a board.
"""
ext_cfgs = []
int_cfgs = []
for name, c in self.iteritems():
if c['boards'] and (board is None or board in c['boards']):
if (name.endswith('-%s' % CONFIG_TYPE_RELEASE) and
c['internal']):
int_cfgs.append(c.deepcopy())
elif (name.endswith('-%s' % CONFIG_TYPE_FULL) and
not c['internal']):
ext_cfgs.append(c.deepcopy())
return ext_cfgs, int_cfgs
def FindCanonicalConfigForBoard(self, board, allow_internal=True):
"""Get the canonical cbuildbot builder config for a board."""
ext_cfgs, int_cfgs = self.FindFullConfigsForBoard(board)
# If both external and internal builds exist for this board, prefer the
# internal one unless instructed otherwise.
both = (int_cfgs if allow_internal else []) + ext_cfgs
if not both:
raise ValueError('Invalid board specified: %s.' % board)
return both[0]
def GetSlavesForMaster(self, master_config, options=None):
"""Gets the important slave builds corresponding to this master.
A slave config is one that matches the master config in build_type,
chrome_rev, and branch. It also must be marked important. For the
full requirements see the logic in code below.
The master itself is eligible to be a slave (of itself) if it has boards.
TODO(dgarrett): Replace this with explicit master/slave defitions to make
the concept less Chrome OS specific. crbug.com/492382.
Args:
master_config: A build config for a master builder.
options: The options passed on the commandline. This argument is optional,
and only makes sense when called from cbuildbot.
Returns:
A list of build configs corresponding to the slaves for the master
represented by master_config.
Raises:
AssertionError if the given config is not a master config or it does
not have a manifest_version.
"""
assert master_config['manifest_version']
assert master_config['master']
slave_configs = []
if options is not None and options.remote_trybot:
return slave_configs
# TODO(davidjames): In CIDB the master isn't considered a slave of itself,
# so we probably shouldn't consider it a slave here either.
for build_config in self.itervalues():
if (build_config['important'] and
build_config['manifest_version'] and
(not build_config['master'] or build_config['boards']) and
build_config['build_type'] == master_config['build_type'] and
build_config['chrome_rev'] == master_config['chrome_rev'] and
build_config['branch'] == master_config['branch']):
slave_configs.append(build_config)
return slave_configs
#
# Methods used when creating a Config programatically.
#
def AddConfig(self, config, name, *args, **kwargs):
"""Derive and add the config to cbuildbot's usable config targets
Args:
config: BuildConfig to derive the new config from.
name: The name to label this configuration; this is what cbuildbot
would see.
args: See the docstring of derive.
kwargs: See the docstring of derive.
Returns:
See the docstring of derive.
"""
inherits, overrides = args, kwargs
overrides['name'] = name
# Add ourselves into the global dictionary, adding in the defaults.
new_config = config.derive(*inherits, **overrides)
self[name] = self.GetDefault().derive(config, new_config)
# Return a BuildConfig object without the defaults, so that other objects
# can derive from us without inheriting the defaults.
return new_config
def AddRawConfig(self, name, *args, **kwargs):
"""Add a config containing only explicitly listed values (no defaults)."""
return self.AddConfig(BuildConfig(), name, *args, **kwargs)
def AddGroup(self, name, *args, **kwargs):
"""Create a new group of build configurations.
Args:
name: The name to label this configuration; this is what cbuildbot
would see.
args: Configurations to build in this group. The first config in
the group is considered the primary configuration and is used
for syncing and creating the chroot.
kwargs: Override values to use for the parent config.
Returns:
A new BuildConfig instance.
"""
child_configs = [self.GetDefault().derive(x, grouped=True) for x in args]
return self.AddConfig(args[0], name, child_configs=child_configs, **kwargs)
def SaveConfigToFile(self, config_file):
"""Save this Config to a Json file.
Args:
config_file: The file to write too.
"""
json_string = self.SaveConfigToString()
osutils.WriteFile(config_file, json_string)
def SaveConfigToString(self):
"""Save this Config object to a Json format string."""
default = self.GetDefault()
config_dict = {}
for k, v in self.iteritems():
config_dict[k] = v.HideDefaults(default)
config_dict['_default'] = default
class _JSONEncoder(json.JSONEncoder):
"""Json Encoder that encodes objects as their dictionaries."""
# pylint: disable=E0202
def default(self, obj):
return self.encode(obj.__dict__)
return json.dumps(config_dict, cls=_JSONEncoder,
sort_keys=True, indent=4, separators=(',', ': '))
#
# Methods related to loading/saving Json.
#
def CreateConfigFromFile(config_file):
"""Load a Config a Json encoded file."""
json_string = osutils.ReadFile(config_file)
return CreateConfigFromString(json_string)
def CreateConfigFromString(json_string):
"""Load a cbuildbot config from it's Json encoded string."""
config_dict = json.loads(json_string, object_hook=_DecodeDict)
# default is a dictionary of default build configuration values.
defaults = config_dict.pop(DEFAULT_BUILD_CONFIG)
defaultBuildConfig = BuildConfig(**defaults)
builds = {n: _CreateBuildConfig(defaultBuildConfig, v)
for n, v in config_dict.iteritems()}
# config is the struct that holds the complete cbuildbot config.
result = SiteConfig(defaults=defaults)
result.update(builds)
return result
# TODO(dgarrett): Remove Decode methods when we prove unicde strings work.
def _DecodeList(data):
"""Convert a JSON result list from unicode to utf-8."""
rv = []
for item in data:
if isinstance(item, unicode):
item = item.encode('utf-8')
elif isinstance(item, list):
item = _DecodeList(item)
elif isinstance(item, dict):
item = _DecodeDict(item)
# Other types (None, int, float, etc) are stored unmodified.
rv.append(item)
return rv
def _DecodeDict(data):
"""Convert a JSON result dict from unicode to utf-8."""
rv = {}
for key, value in data.iteritems():
if isinstance(key, unicode):
key = key.encode('utf-8')
if isinstance(value, unicode):
value = value.encode('utf-8')
elif isinstance(value, list):
value = _DecodeList(value)
elif isinstance(value, dict):
value = _DecodeDict(value)
# Other types (None, int, float, etc) are stored unmodified.
rv[key] = value
return rv
def _CreateHwTestConfig(jsonString):
"""Create a HWTestConfig object from a JSON string."""
# Each HW Test is dumped as a json string embedded in json.
hw_test_config = json.loads(jsonString, object_hook=_DecodeDict)
return HWTestConfig(**hw_test_config)
def _CreateBuildConfig(default, build_dict):
"""Create a BuildConfig object from it's parsed JSON dictionary encoding."""
# These build config values need special handling.
hwtests = build_dict.pop('hw_tests', None)
child_configs = build_dict.pop('child_configs', None)
result = default.derive(**build_dict)
if hwtests is not None:
result['hw_tests'] = [_CreateHwTestConfig(hwtest) for hwtest in hwtests]
if child_configs is not None:
result['child_configs'] = [_CreateBuildConfig(default, child)
for child in child_configs]
return result