# Lint as: python2, python3 """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. import collections import os import re import six import six.moves.configparser as ConfigParser import sys from six.moves import StringIO from autotest_lib.client.common_lib import error from autotest_lib.client.common_lib import lsbrelease_utils from autotest_lib.client.common_lib import seven 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 = seven.config_parser() 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 _load_config_file(self, config_file): """ Load the config_file into a StringIO buffer parsable by the current py version. TODO b:179407161, when running only in Python 3, force config files to be correct, and remove this special parsing. When in Python 3, this will change instances of %, not followed immediately by (, to %%. Thus: "%foo" --> "%%foo" "%(foo" --> "%(foo" "%%foo" --> "%%foo" In Python 2, we will do the opposite, and change instances of %%, to %. "%%foo" --> "%foo" "%%(foo" --> "%(foo" "%foo" --> "%foo" """ with open(config_file) as cf: config_file_str = cf.read() if six.PY3: config_file_str = re.sub(r"([^%]|^)%([^%(]|$)", r"\1%%\2", config_file_str) else: config_file_str = config_file_str.replace('%%', '%') return StringIO(config_file_str) def _read_config(self, config, buf): """Read the provided io buffer, into the specified config.""" if six.PY3: config.read_file(buf) else: config.readfp(buf) def parse_config_file(self): """Parse config files.""" self.config = seven.config_parser() if self.config_file and os.path.exists(self.config_file): buf = self._load_config_file(self.config_file) self._read_config(self.config, buf) 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 = seven.config_parser() mob_buf = self._load_config_file(self.moblab_file) self._read_config(moblab_config, mob_buf) # 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 = seven.config_parser() shadow_buf = self._load_config_file(self.shadow_file) self._read_config(shadow_config, shadow_buf) # 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