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