blob: 545045e4a0ced25b3c58dcc12541182ab030ee65 [file] [log] [blame]
"""A singleton class for accessing global config values
provides access to global configuration file
"""
# The config values can be stored in 3 config files:
# global_config.ini
# moblab_config.ini
# shadow_config.ini
# When the code is running in Moblab, config values in moblab config override
# values in global config, and config values in shadow config override values
# in both moblab and global config.
# When the code is running in a non-Moblab host, moblab_config.ini is ignored.
# Config values in shadow config will override values in global config.
__author__ = 'raphtee@google.com (Travis Miller)'
import collections
import ConfigParser
import os
import re
import sys
from autotest_lib.client.common_lib import error
from autotest_lib.client.common_lib import lsbrelease_utils
class ConfigError(error.AutotestError):
"""Configuration error."""
pass
class ConfigValueError(ConfigError):
"""Configuration value error, raised when value failed to be converted to
expected type."""
pass
common_lib_dir = os.path.dirname(sys.modules[__name__].__file__)
client_dir = os.path.dirname(common_lib_dir)
root_dir = os.path.dirname(client_dir)
# Check if the config files are at autotest's root dir
# This will happen if client is executing inside a full autotest tree, or if
# other entry points are being executed
global_config_path_root = os.path.join(root_dir, 'global_config.ini')
moblab_config_path_root = os.path.join(root_dir, 'moblab_config.ini')
shadow_config_path_root = os.path.join(root_dir, 'shadow_config.ini')
config_in_root = os.path.exists(global_config_path_root)
# Check if the config files are at autotest's client dir
# This will happen if a client stand alone execution is happening
global_config_path_client = os.path.join(client_dir, 'global_config.ini')
config_in_client = os.path.exists(global_config_path_client)
if config_in_root:
DEFAULT_CONFIG_FILE = global_config_path_root
if os.path.exists(moblab_config_path_root):
DEFAULT_MOBLAB_FILE = moblab_config_path_root
else:
DEFAULT_MOBLAB_FILE = None
if os.path.exists(shadow_config_path_root):
DEFAULT_SHADOW_FILE = shadow_config_path_root
else:
DEFAULT_SHADOW_FILE = None
RUNNING_STAND_ALONE_CLIENT = False
elif config_in_client:
DEFAULT_CONFIG_FILE = global_config_path_client
DEFAULT_MOBLAB_FILE = None
DEFAULT_SHADOW_FILE = None
RUNNING_STAND_ALONE_CLIENT = True
else:
DEFAULT_CONFIG_FILE = None
DEFAULT_MOBLAB_FILE = None
DEFAULT_SHADOW_FILE = None
RUNNING_STAND_ALONE_CLIENT = True
class global_config_class(object):
"""Object to access config values."""
_NO_DEFAULT_SPECIFIED = object()
_config = None
config_file = DEFAULT_CONFIG_FILE
moblab_file=DEFAULT_MOBLAB_FILE
shadow_file = DEFAULT_SHADOW_FILE
running_stand_alone_client = RUNNING_STAND_ALONE_CLIENT
@property
def config(self):
"""ConfigParser instance.
If the instance dict doesn't have a config key, this descriptor
will be called to ensure the config file is parsed (setting the
config key in the instance dict as a side effect). Once the
instance dict has a config key, that value will be used in
preference.
"""
if self._config is None:
self.parse_config_file()
return self._config
@config.setter
def config(self, value):
"""Set config attribute.
@param value: value to set
"""
self._config = value
def check_stand_alone_client_run(self):
"""Check if this is a stand alone client that does not need config."""
return self.running_stand_alone_client
def set_config_files(self, config_file=DEFAULT_CONFIG_FILE,
shadow_file=DEFAULT_SHADOW_FILE,
moblab_file=DEFAULT_MOBLAB_FILE):
self.config_file = config_file
self.moblab_file = moblab_file
self.shadow_file = shadow_file
self._config = None
def _handle_no_value(self, section, key, default):
if default is self._NO_DEFAULT_SPECIFIED:
msg = ("Value '%s' not found in section '%s'" %
(key, section))
raise ConfigError(msg)
else:
return default
def get_section_as_dict(self, section):
"""Return a dict mapping section options to values.
This is useful if a config section is being used like a
dictionary. If the section is missing, return an empty dict.
This returns an OrderedDict, preserving the order of the options
in the section.
@param section: Section to get.
@return: OrderedDict
"""
if self.config.has_section(section):
return collections.OrderedDict(self.config.items(section))
else:
return collections.OrderedDict()
def get_section_values(self, section):
"""
Return a config parser object containing a single section of the
global configuration, that can be later written to a file object.
@param section: Section we want to turn into a config parser object.
@return: ConfigParser() object containing all the contents of section.
"""
cfgparser = ConfigParser.ConfigParser()
cfgparser.add_section(section)
for option, value in self.config.items(section):
cfgparser.set(section, option, value)
return cfgparser
def get_config_value(self, section, key, type=str,
default=_NO_DEFAULT_SPECIFIED, allow_blank=False):
"""Get a configuration value
@param section: Section the key is in.
@param key: The key to look up.
@param type: The expected type of the returned value.
@param default: A value to return in case the key couldn't be found.
@param allow_blank: If False, an empty string as a value is treated like
there was no value at all. If True, empty strings
will be returned like they were normal values.
@raises ConfigError: If the key could not be found and no default was
specified.
@return: The obtained value or default.
"""
try:
val = self.config.get(section, key)
except ConfigParser.Error:
return self._handle_no_value(section, key, default)
if not val.strip() and not allow_blank:
return self._handle_no_value(section, key, default)
return self._convert_value(key, section, val, type)
def get_config_value_regex(self, section, key_regex, type=str):
"""Get a dict of configs in given section with key matched to key-regex.
@param section: Section the key is in.
@param key_regex: The regex that key should match.
@param type: data type the value should have.
@return: A dictionary of key:value with key matching `key_regex`. Return
an empty dictionary if no matching key is found.
"""
configs = {}
for option, value in self.config.items(section):
if re.match(key_regex, option):
configs[option] = self._convert_value(option, section, value,
type)
return configs
# This order of parameters ensures this can be called similar to the normal
# get_config_value which is mostly called with (section, key, type).
def get_config_value_with_fallback(self, section, key, fallback_key,
type=str, fallback_section=None,
default=_NO_DEFAULT_SPECIFIED, **kwargs):
"""Get a configuration value if it exists, otherwise use fallback.
Tries to obtain a configuration value for a given key. If this value
does not exist, the value looked up under a different key will be
returned.
@param section: Section the key is in.
@param key: The key to look up.
@param fallback_key: The key to use in case the original key wasn't
found.
@param type: data type the value should have.
@param fallback_section: The section the fallback key resides in. In
case none is specified, the the same section as
for the primary key is used.
@param default: Value to return if values could neither be obtained for
the key nor the fallback key.
@param **kwargs: Additional arguments that should be passed to
get_config_value.
@raises ConfigError: If the fallback key doesn't exist and no default
was provided.
@return: The value that was looked up for the key. If that didn't
exist, the value looked up for the fallback key will be
returned. If that also didn't exist, default will be returned.
"""
if fallback_section is None:
fallback_section = section
try:
return self.get_config_value(section, key, type, **kwargs)
except ConfigError:
return self.get_config_value(fallback_section, fallback_key,
type, default=default, **kwargs)
def override_config_value(self, section, key, new_value):
"""Override a value from the config file with a new value.
@param section: Name of the section.
@param key: Name of the key.
@param new_value: new value.
"""
self.config.set(section, key, new_value)
def reset_config_values(self):
"""
Reset all values to those found in the config files (undoes all
overrides).
"""
self.parse_config_file()
def merge_configs(self, override_config):
"""Merge existing config values with the ones in given override_config.
@param override_config: Configs to override existing config values.
"""
# overwrite whats in config with whats in override_config
sections = override_config.sections()
for section in sections:
# add the section if need be
if not self.config.has_section(section):
self.config.add_section(section)
# now run through all options and set them
options = override_config.options(section)
for option in options:
val = override_config.get(section, option)
self.config.set(section, option, val)
def parse_config_file(self):
"""Parse config files."""
self.config = ConfigParser.ConfigParser()
if self.config_file and os.path.exists(self.config_file):
self.config.read(self.config_file)
else:
raise ConfigError('%s not found' % (self.config_file))
# If it's running in Moblab, read moblab config file if exists,
# overwrite the value in global config.
if (lsbrelease_utils.is_moblab() and self.moblab_file and
os.path.exists(self.moblab_file)):
moblab_config = ConfigParser.ConfigParser()
moblab_config.read(self.moblab_file)
# now we merge moblab into global
self.merge_configs(moblab_config)
# now also read the shadow file if there is one
# this will overwrite anything that is found in the
# other config
if self.shadow_file and os.path.exists(self.shadow_file):
shadow_config = ConfigParser.ConfigParser()
shadow_config.read(self.shadow_file)
# now we merge shadow into global
self.merge_configs(shadow_config)
# the values that are pulled from ini
# are strings. But we should attempt to
# convert them to other types if needed.
def _convert_value(self, key, section, value, value_type):
# strip off leading and trailing white space
sval = value.strip()
# if length of string is zero then return None
if len(sval) == 0:
if value_type == str:
return ""
elif value_type == bool:
return False
elif value_type == int:
return 0
elif value_type == float:
return 0.0
elif value_type == list:
return []
else:
return None
if value_type == bool:
if sval.lower() == "false":
return False
else:
return True
if value_type == list:
# Split the string using ',' and return a list
return [val.strip() for val in sval.split(',')]
try:
conv_val = value_type(sval)
return conv_val
except:
msg = ("Could not convert %s value %r in section %s to type %s" %
(key, sval, section, value_type))
raise ConfigValueError(msg)
def get_sections(self):
"""Return a list of sections available."""
return self.config.sections()
# insure the class is a singleton. Now the symbol global_config
# will point to the one and only one instace of the class
global_config = global_config_class()
class FakeGlobalConfig(object):
"""Fake replacement for global_config singleton object.
Unittest will want to fake the global_config so that developers'
shadow_config doesn't leak into unittests. Provide a fake object for that
purpose.
"""
# pylint: disable=missing-docstring
def __init__(self):
self._config_info = {}
def set_config_value(self, section, key, value):
self._config_info[(section, key)] = value
def get_config_value(self, section, key, type=str,
default=None, allow_blank=False):
identifier = (section, key)
if identifier not in self._config_info:
return default
return self._config_info[identifier]
def parse_config_file(self):
pass