xref: /aosp_15_r20/external/autotest/client/common_lib/global_config.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
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