1# Lint as: python2, python3 2"""A singleton class for accessing global config values 3 4provides access to global configuration file 5""" 6 7# The config values can be stored in 3 config files: 8# global_config.ini 9# moblab_config.ini 10# shadow_config.ini 11# When the code is running in Moblab, config values in moblab config override 12# values in global config, and config values in shadow config override values 13# in both moblab and global config. 14# When the code is running in a non-Moblab host, moblab_config.ini is ignored. 15# Config values in shadow config will override values in global config. 16 17import collections 18import os 19import re 20import six.moves.configparser as ConfigParser 21import sys 22 23from autotest_lib.client.common_lib import error 24from autotest_lib.client.common_lib import lsbrelease_utils 25from autotest_lib.client.common_lib import seven 26 27 28class ConfigError(error.AutotestError): 29 """Configuration error.""" 30 pass 31 32 33class ConfigValueError(ConfigError): 34 """Configuration value error, raised when value failed to be converted to 35 expected type.""" 36 pass 37 38 39 40common_lib_dir = os.path.dirname(sys.modules[__name__].__file__) 41client_dir = os.path.dirname(common_lib_dir) 42root_dir = os.path.dirname(client_dir) 43 44# Check if the config files are at autotest's root dir 45# This will happen if client is executing inside a full autotest tree, or if 46# other entry points are being executed 47global_config_path_root = os.path.join(root_dir, 'global_config.ini') 48moblab_config_path_root = os.path.join(root_dir, 'moblab_config.ini') 49shadow_config_path_root = os.path.join(root_dir, 'shadow_config.ini') 50config_in_root = os.path.exists(global_config_path_root) 51 52# Check if the config files are at autotest's client dir 53# This will happen if a client stand alone execution is happening 54global_config_path_client = os.path.join(client_dir, 'global_config.ini') 55config_in_client = os.path.exists(global_config_path_client) 56 57if config_in_root: 58 DEFAULT_CONFIG_FILE = global_config_path_root 59 if os.path.exists(moblab_config_path_root): 60 DEFAULT_MOBLAB_FILE = moblab_config_path_root 61 else: 62 DEFAULT_MOBLAB_FILE = None 63 if os.path.exists(shadow_config_path_root): 64 DEFAULT_SHADOW_FILE = shadow_config_path_root 65 else: 66 DEFAULT_SHADOW_FILE = None 67 RUNNING_STAND_ALONE_CLIENT = False 68elif config_in_client: 69 DEFAULT_CONFIG_FILE = global_config_path_client 70 DEFAULT_MOBLAB_FILE = None 71 DEFAULT_SHADOW_FILE = None 72 RUNNING_STAND_ALONE_CLIENT = True 73else: 74 DEFAULT_CONFIG_FILE = None 75 DEFAULT_MOBLAB_FILE = None 76 DEFAULT_SHADOW_FILE = None 77 RUNNING_STAND_ALONE_CLIENT = True 78 79 80class global_config_class(object): 81 """Object to access config values.""" 82 _NO_DEFAULT_SPECIFIED = object() 83 84 _config = None 85 config_file = DEFAULT_CONFIG_FILE 86 moblab_file=DEFAULT_MOBLAB_FILE 87 shadow_file = DEFAULT_SHADOW_FILE 88 running_stand_alone_client = RUNNING_STAND_ALONE_CLIENT 89 90 91 @property 92 def config(self): 93 """ConfigParser instance. 94 95 If the instance dict doesn't have a config key, this descriptor 96 will be called to ensure the config file is parsed (setting the 97 config key in the instance dict as a side effect). Once the 98 instance dict has a config key, that value will be used in 99 preference. 100 """ 101 if self._config is None: 102 self.parse_config_file() 103 return self._config 104 105 106 @config.setter 107 def config(self, value): 108 """Set config attribute. 109 110 @param value: value to set 111 """ 112 self._config = value 113 114 115 def check_stand_alone_client_run(self): 116 """Check if this is a stand alone client that does not need config.""" 117 return self.running_stand_alone_client 118 119 120 def set_config_files(self, config_file=DEFAULT_CONFIG_FILE, 121 shadow_file=DEFAULT_SHADOW_FILE, 122 moblab_file=DEFAULT_MOBLAB_FILE): 123 self.config_file = config_file 124 self.moblab_file = moblab_file 125 self.shadow_file = shadow_file 126 self._config = None 127 128 129 def _handle_no_value(self, section, key, default): 130 if default is self._NO_DEFAULT_SPECIFIED: 131 msg = ("Value '%s' not found in section '%s'" % 132 (key, section)) 133 raise ConfigError(msg) 134 else: 135 return default 136 137 138 def get_section_as_dict(self, section): 139 """Return a dict mapping section options to values. 140 141 This is useful if a config section is being used like a 142 dictionary. If the section is missing, return an empty dict. 143 144 This returns an OrderedDict, preserving the order of the options 145 in the section. 146 147 @param section: Section to get. 148 @return: OrderedDict 149 """ 150 if self.config.has_section(section): 151 return collections.OrderedDict(self.config.items(section)) 152 else: 153 return collections.OrderedDict() 154 155 156 def get_section_values(self, section): 157 """ 158 Return a config parser object containing a single section of the 159 global configuration, that can be later written to a file object. 160 161 @param section: Section we want to turn into a config parser object. 162 @return: ConfigParser() object containing all the contents of section. 163 """ 164 cfgparser = seven.config_parser() 165 cfgparser.add_section(section) 166 for option, value in self.config.items(section): 167 cfgparser.set(section, option, value) 168 return cfgparser 169 170 171 def get_config_value(self, section, key, type=str, 172 default=_NO_DEFAULT_SPECIFIED, allow_blank=False): 173 """Get a configuration value 174 175 @param section: Section the key is in. 176 @param key: The key to look up. 177 @param type: The expected type of the returned value. 178 @param default: A value to return in case the key couldn't be found. 179 @param allow_blank: If False, an empty string as a value is treated like 180 there was no value at all. If True, empty strings 181 will be returned like they were normal values. 182 183 @raises ConfigError: If the key could not be found and no default was 184 specified. 185 186 @return: The obtained value or default. 187 """ 188 try: 189 val = self.config.get(section, key) 190 except ConfigParser.Error: 191 return self._handle_no_value(section, key, default) 192 193 if not val.strip() and not allow_blank: 194 return self._handle_no_value(section, key, default) 195 196 return self._convert_value(key, section, val, type) 197 198 199 def get_config_value_regex(self, section, key_regex, type=str): 200 """Get a dict of configs in given section with key matched to key-regex. 201 202 @param section: Section the key is in. 203 @param key_regex: The regex that key should match. 204 @param type: data type the value should have. 205 206 @return: A dictionary of key:value with key matching `key_regex`. Return 207 an empty dictionary if no matching key is found. 208 """ 209 configs = {} 210 for option, value in self.config.items(section): 211 if re.match(key_regex, option): 212 configs[option] = self._convert_value(option, section, value, 213 type) 214 return configs 215 216 217 # This order of parameters ensures this can be called similar to the normal 218 # get_config_value which is mostly called with (section, key, type). 219 def get_config_value_with_fallback(self, section, key, fallback_key, 220 type=str, fallback_section=None, 221 default=_NO_DEFAULT_SPECIFIED, **kwargs): 222 """Get a configuration value if it exists, otherwise use fallback. 223 224 Tries to obtain a configuration value for a given key. If this value 225 does not exist, the value looked up under a different key will be 226 returned. 227 228 @param section: Section the key is in. 229 @param key: The key to look up. 230 @param fallback_key: The key to use in case the original key wasn't 231 found. 232 @param type: data type the value should have. 233 @param fallback_section: The section the fallback key resides in. In 234 case none is specified, the the same section as 235 for the primary key is used. 236 @param default: Value to return if values could neither be obtained for 237 the key nor the fallback key. 238 @param **kwargs: Additional arguments that should be passed to 239 get_config_value. 240 241 @raises ConfigError: If the fallback key doesn't exist and no default 242 was provided. 243 244 @return: The value that was looked up for the key. If that didn't 245 exist, the value looked up for the fallback key will be 246 returned. If that also didn't exist, default will be returned. 247 """ 248 if fallback_section is None: 249 fallback_section = section 250 251 try: 252 return self.get_config_value(section, key, type, **kwargs) 253 except ConfigError: 254 return self.get_config_value(fallback_section, fallback_key, 255 type, default=default, **kwargs) 256 257 258 def override_config_value(self, section, key, new_value): 259 """Override a value from the config file with a new value. 260 261 @param section: Name of the section. 262 @param key: Name of the key. 263 @param new_value: new value. 264 """ 265 self.config.set(section, key, new_value) 266 267 268 def reset_config_values(self): 269 """ 270 Reset all values to those found in the config files (undoes all 271 overrides). 272 """ 273 self.parse_config_file() 274 275 276 def merge_configs(self, override_config): 277 """Merge existing config values with the ones in given override_config. 278 279 @param override_config: Configs to override existing config values. 280 """ 281 # overwrite whats in config with whats in override_config 282 sections = override_config.sections() 283 for section in sections: 284 # add the section if need be 285 if not self.config.has_section(section): 286 self.config.add_section(section) 287 # now run through all options and set them 288 options = override_config.options(section) 289 for option in options: 290 val = override_config.get(section, option) 291 self.config.set(section, option, val) 292 293 294 def parse_config_file(self): 295 """Parse config files.""" 296 self.config = seven.config_parser() 297 if self.config_file and os.path.exists(self.config_file): 298 self.config.read(self.config_file) 299 else: 300 raise ConfigError('%s not found' % (self.config_file)) 301 302 # If it's running in Moblab, read moblab config file if exists, 303 # overwrite the value in global config. 304 if (lsbrelease_utils.is_moblab() and self.moblab_file and 305 os.path.exists(self.moblab_file)): 306 moblab_config = seven.config_parser() 307 moblab_config.read(self.moblab_file) 308 # now we merge moblab into global 309 self.merge_configs(moblab_config) 310 311 # now also read the shadow file if there is one 312 # this will overwrite anything that is found in the 313 # other config 314 if self.shadow_file and os.path.exists(self.shadow_file): 315 shadow_config = seven.config_parser() 316 shadow_config.read(self.shadow_file) 317 # now we merge shadow into global 318 self.merge_configs(shadow_config) 319 320 321 # the values that are pulled from ini 322 # are strings. But we should attempt to 323 # convert them to other types if needed. 324 def _convert_value(self, key, section, value, value_type): 325 # strip off leading and trailing white space 326 sval = value.strip() 327 328 # if length of string is zero then return None 329 if len(sval) == 0: 330 if value_type == str: 331 return "" 332 elif value_type == bool: 333 return False 334 elif value_type == int: 335 return 0 336 elif value_type == float: 337 return 0.0 338 elif value_type == list: 339 return [] 340 else: 341 return None 342 343 if value_type == bool: 344 if sval.lower() == "false": 345 return False 346 else: 347 return True 348 349 if value_type == list: 350 # Split the string using ',' and return a list 351 return [val.strip() for val in sval.split(',')] 352 353 try: 354 conv_val = value_type(sval) 355 return conv_val 356 except: 357 msg = ("Could not convert %s value %r in section %s to type %s" % 358 (key, sval, section, value_type)) 359 raise ConfigValueError(msg) 360 361 362 def get_sections(self): 363 """Return a list of sections available.""" 364 return self.config.sections() 365 366 367# insure the class is a singleton. Now the symbol global_config 368# will point to the one and only one instace of the class 369global_config = global_config_class() 370 371 372class FakeGlobalConfig(object): 373 """Fake replacement for global_config singleton object. 374 375 Unittest will want to fake the global_config so that developers' 376 shadow_config doesn't leak into unittests. Provide a fake object for that 377 purpose. 378 379 """ 380 # pylint: disable=missing-docstring 381 382 def __init__(self): 383 self._config_info = {} 384 385 386 def set_config_value(self, section, key, value): 387 self._config_info[(section, key)] = value 388 389 390 def get_config_value(self, section, key, type=str, 391 default=None, allow_blank=False): 392 identifier = (section, key) 393 if identifier not in self._config_info: 394 return default 395 return self._config_info[identifier] 396 397 398 def parse_config_file(self): 399 pass 400