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