| # Copyright (c) 2013 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """ |
| Configurable: A base class. Provides methods to read, and verify configuration. |
| |
| read_config_from_file: Reads a config text file and returns a config object |
| |
| get_section_of_config: Returns a subsection of the configspec |
| |
| get_section_of_configspec: Returns a subsection of the config |
| |
| ConfigurationError: The exception type thrown by the Configurable functions |
| """ |
| |
| import collections |
| import pprint |
| import re |
| from wireless_automation.aspects import configobj |
| from wireless_automation.aspects import wireless_automation_error |
| from wireless_automation.aspects import wireless_automation_logging |
| from wireless_automation.aspects import validate |
| |
| log = wireless_automation_logging.setup_logging('configurable') |
| |
| |
| class ConfigurationError(wireless_automation_error.WirelessAutomationError): |
| pass |
| |
| |
| def read_config_from_file(file_name): |
| """ |
| Read a config from a file. |
| |
| @param file_name: The file to read. |
| |
| @return: a ConfigObj containing the data. |
| This will have no configspec, only the data. |
| |
| """ |
| return configobj.ConfigObj(file_name) |
| |
| |
| def get_section_of_configspec(configspec, section_name): |
| """ |
| Returns the specified subsection of the configspec. |
| |
| @param configspec: The configspec |
| |
| @param section_name: The name of the section to return |
| |
| @return: A subsection of the passed in configspec. |
| |
| """ |
| if isinstance(configspec, configobj.ConfigObj): |
| pass |
| elif isinstance(configspec, list): |
| configspec = configobj.ConfigObj(configspec=configspec).configspec |
| else: |
| raise ConfigurationError('get_section_of_configspec needs ConfigObj' |
| ' or list, not %s ' % type(configspec)) |
| |
| if section_name not in configspec.sections: |
| raise ConfigurationError('Subsection %s not found in configspec %s' % |
| (section_name, configspec)) |
| # A bit of a hack. To retain comments in the configspec, we |
| # cast it to a ConfigObj, and write it. |
| strings = configobj.ConfigObj(configspec[section_name]).write() |
| # It produces a correct output, except for not removing the [] |
| # of child sections. So we do that here, along with removing quotes. |
| strings = [re.sub('\"(.*)\"', '\\1', x) for x in strings] |
| strings = [re.sub('\[(.*)\]', '\\1', x) for x in strings] |
| return strings |
| |
| |
| def get_section_of_config(config, section_name): |
| """ |
| Returns one section of the config, by name. |
| |
| Used for reading in the config file for the app, which contains config |
| for many objects. Pull out the config for each object by section name. |
| |
| Output: |
| [Class1Section] |
| power=33 |
| channel=3 |
| [Class2Section] |
| power=33 |
| freq=1889.8 |
| |
| @param config: The config to copy the section from |
| |
| @param section_name: the top level section name |
| |
| @return: The section as a ConfigObj |
| |
| """ |
| x = config[section_name] |
| config = configobj.ConfigObj(x).write() |
| config = [re.sub('\[(.*)\]', '\\1', x) for x in config] |
| return config |
| |
| |
| def combine_configs(tuple_list): |
| """ |
| Combines configs. Used when asking many objects for their config, then |
| saving the combined config in one file that is now the config for the app. |
| Duplicate section names are bad. |
| |
| Example: |
| c1 = class1.get_default_config() |
| c2 = class2.get_default_config() |
| combined_config = [('Class1Section',c1),('Class2Section',c2)] |
| combined_config.print() |
| |
| Output: |
| [Class1Section] |
| power=33 |
| channel=3 |
| [Class2Section] |
| power=33 |
| freq=1889.8 |
| |
| @param tuple_list: list of ('name',ConfigObj) |
| |
| @return: a ConfigObj composed of the sub sections, while maintaining order. |
| |
| """ |
| # Check the input |
| sections = [x[0] for x in tuple_list] |
| counter = collections.Counter(sections) |
| if counter.most_common()[0][1] > 1: |
| log.error('conbine_configs requires unique names. These are duplicates') |
| for name in counter.most_common(): |
| if name[1] > 1: |
| log.error("'%s' was used %s times" % (name[0], name[1])) |
| raise ConfigurationError('combine_configs called with duplicate names') |
| # do the merge |
| merged = configobj.ConfigObj() |
| for item in tuple_list: |
| assert isinstance(item[0], str) |
| assert isinstance(item[1], configobj.ConfigObj) |
| merged.merge({item[0]: item[1].dict()}) |
| return merged |
| |
| |
| class Configurable(object): |
| """ |
| Provides an interface for configuring complex objects. The specification |
| for the config is specified in self.configspec, and incoming config |
| is checked against this. This provides a single location to specify what |
| input an object needs, and a way to verify the incoming config. |
| |
| Public API: |
| get_default_config() |
| is_this_config_valid() |
| |
| The derived class must: |
| Specify a self.configspec as a class variable. |
| Take in a config object as a parameter to __init__, and |
| Call the super constructor: Configurable.__init__() |
| |
| Configspec Examples: |
| |
| class MyNewClass(Configurable): |
| configspec = [ |
| 'channel=integer(min=0, max=9999, default=1)', |
| 'scpi_hostname=string(default=lookup_in_labconfig)', |
| 'port=integer(default=1234)', |
| 'gpib_address=integer(min=0, max=999, default=14)', |
| 'read_timeout_seconds=integer(min=0, max=100, default=3)', |
| 'connect_timeout_seconds=integer(min=0, max=100, default=10)' |
| ] |
| |
| class ClassWithAllTheConfigTypes(Configurable): |
| configspec = [ |
| "Number = integer(min=0, max=10, default=9)", |
| "Float = float (min='0', max='1.111', default = '1.1')", |
| "TrueFalse = boolean(default=False)", |
| "IpAddress = ip_addr(default='10.0.0.1')", |
| "String = string(min=0,max=100, default='100 chars max')", |
| "Tuple = tuple(min=1, max=2, default=list('a','b'))", |
| "IntList = int_list(min=1, max=10, default =list(1,2))", |
| "FloatList = float_list(min=1, max=10, default =list('1.1',2))", |
| "BoolList = bool_list(min=1, max=10, " |
| "default =list(True,False,0,'yes',no))", |
| "IpAddrList = ip_addr_list(min=1, max=10, " |
| "default =list(10.10.10.1,1.1.1.1))", |
| "StringList = string_list(min=1, max=10, " |
| "default =list('string1', 'string2'))", |
| #Mixed list does not have the default keyword, making this |
| # not useful for Configurable, which uses default extensively. |
| #"MixedList = Do not use. |
| "AlwaysOK = pass(default = None) ", |
| "OptionList = option('red', 'blue', 'green',default='red' )", |
| ] |
| |
| Usage Example: |
| config = complex_object_class.get_default_config() |
| config['only param I want to change'] = 'new and better value' |
| if complex_object_class.is_this_config_valid(config): |
| co = complex_obj(config) |
| |
| A list of the available types from the configobj docs. |
| |
| * 'integer': matches integer values (including negative) |
| Takes optional 'min' and 'max' arguments : :: |
| |
| integer() |
| integer(3, 9) # any value from 3 to 9 |
| integer(min=0) # any positive value |
| integer(max=9) |
| |
| * 'float': matches float values |
| Has the same parameters as the integer check. |
| |
| * 'boolean': matches boolean values - ``True`` or ``False`` |
| Acceptable string values for True are : |
| true, on, yes, 1 |
| Acceptable string values for False are : |
| false, off, no, 0 |
| |
| Any other value raises an error. |
| |
| * 'ip_addr': matches an Internet Protocol address, v.4, represented |
| by a dotted-quad string, i.e. '1.2.3.4'. |
| |
| * 'string': matches any string. |
| Takes optional keyword args 'min' and 'max' |
| to specify min and max lengths of the string. |
| |
| * 'list': matches any list. |
| Takes optional keyword args 'min', and 'max' to specify min and |
| max sizes of the list. (Always returns a list.) |
| |
| * 'tuple': matches any tuple. |
| Takes optional keyword args 'min', and 'max' to specify min and |
| max sizes of the tuple. (Always returns a tuple.) |
| |
| * 'int_list': Matches a list of integers. |
| Takes the same arguments as list. |
| |
| * 'float_list': Matches a list of floats. |
| Takes the same arguments as list. |
| |
| * 'bool_list': Matches a list of boolean values. |
| Takes the same arguments as list. |
| |
| * 'ip_addr_list': Matches a list of IP addresses. |
| Takes the same arguments as list. |
| |
| * 'string_list': Matches a list of strings. |
| Takes the same arguments as list. |
| |
| #Mixed list does not have the default keyword, making this |
| # not useful for Configurable, which uses defaults extensively. |
| * 'mixed_list': Not usable in Configurable. |
| |
| * 'pass': This check matches everything ! It never fails |
| and the value is unchanged. |
| |
| It is also the default if no check is specified. |
| |
| * 'option': This check matches any from a list of options. |
| You specify this check with : :: |
| |
| option('option 1', 'option 2', 'option 3') |
| |
| """ |
| # The config spec(a ConfigObject term for the definition of the |
| # configuration) |
| CONFIGSPEC = ['Override configspec derived classes'] |
| |
| def __init__(self, input_config_data=None): |
| """ |
| The configurable class defines an interface, but cannot |
| be instantiated. |
| |
| Require the derived class to have self.configspec defined |
| |
| @param: input_config_data: Either a string or a list of string |
| containing the configuration options and data. |
| |
| """ |
| try: |
| # Explicitly require a self.configspec |
| len(self.CONFIGSPEC) > 0 |
| except AttributeError: |
| error_message = \ |
| 'This class needs but does not have a self.configspec' |
| log.critical(error_message) |
| raise ConfigurationError(error_message) |
| |
| # Check the configspec for sanity. |
| self.CONFIGSPEC = self._convert_configspec_to_str_list(self.CONFIGSPEC) |
| |
| # Run get_default_config to verify the configspec is valid |
| # We do this at construction to announce problems as early as possible |
| default_config = self.get_default_config() |
| |
| # To allow for the config to be a subset of all the options, |
| # combine the passed in config with the default values. |
| # This way the incoming config could be empty, or have a single value. |
| input_config_obj = configobj.ConfigObj(input_config_data) |
| config = self._merge_two_configs(default_config, input_config_obj) |
| |
| if self.is_this_config_valid(config): |
| # Convert the string values to ints and floats |
| self.config = self._convert_strings_to_types(config) |
| else: |
| raise ConfigurationError( |
| 'config data does not match the configspec of the object') |
| |
| @classmethod |
| def _merge_two_configs(cls, base_config, extension_config): |
| """ |
| Merges two configs. |
| |
| Example: |
| base_config = {'temperature': 43, 'day': 'monday'} |
| extension_config = {'temperature': 99, 'color': 'red'} |
| returns : {'temperature': 99, 'day': 'monday', 'color': 'red'} |
| |
| @param base_config: ConfigObj |
| |
| @param extension_config: ConfigObj |
| |
| @return: base_config filled in with data from extension_config. |
| |
| |
| """ |
| assert isinstance(base_config, configobj.ConfigObj) |
| assert isinstance(extension_config, configobj.ConfigObj) |
| base_config.merge(extension_config) |
| return base_config |
| |
| @classmethod |
| def _get_items_in_config_not_in_configspec(cls, config): |
| """ |
| @param config: ConfigObj |
| |
| @return: a list of items in config, but not in self.configspec. |
| |
| This happens when a config value is misspelled. |
| |
| """ |
| # Make a new ConfigObj of our configspec and the incoming config |
| new_config = configobj.ConfigObj( |
| dict(config), configspec=cls.CONFIGSPEC) |
| validator = validate.Validator() |
| new_config.validate( |
| validator, |
| preserve_errors=True, copy=True) |
| extra_items = configobj.get_extra_values(new_config) |
| return extra_items |
| |
| def _convert_strings_to_types(self, config): |
| """ |
| @param config: ConfigObj |
| |
| @return: ConfigObj with values converted to the correct types |
| |
| """ |
| assert isinstance(config, configobj.ConfigObj) |
| validator = validate.Validator() |
| config.validate(validator, preserve_errors=True, copy=True) |
| return config |
| |
| @classmethod |
| def is_this_config_valid(cls, config): |
| """ |
| Verify that config passes validation against self.configspec |
| |
| Raises an exception on bad configs. |
| |
| @param config: The config to check |
| |
| @return: True or False |
| |
| """ |
| if not isinstance(config, configobj.ConfigObj): |
| raise ConfigurationError( |
| 'Object passed into _is_this_config_valid is not a ' |
| 'ConfigObj') |
| new_config = configobj.ConfigObj( |
| dict(config), |
| configspec=cls.CONFIGSPEC) |
| validator = validate.Validator() |
| validate_results = new_config.validate(validator, |
| preserve_errors=True, copy=True) |
| extra_items = configobj.get_extra_values(new_config) |
| flattend_errors = configobj.flatten_errors(new_config, validate_results) |
| |
| if (validate_results is True) and not extra_items: |
| return True |
| |
| # An invalid config may have bad keys, which means we can't convert |
| # it to a string. If that happens, just print the keys. |
| try: |
| log.critical('This config is bad: %s ' % config) |
| except KeyError: |
| log.critical('This config with theses keys is bad: %s ' % |
| config.keys()) |
| |
| log.critical('Above config must conform to this configspec: ') |
| pretty_str = pprint.pformat(cls.CONFIGSPEC) |
| log.critical('\n' + pretty_str) |
| if len(extra_items) > 0: |
| log.critical('Extra (mispelled?) items: ') |
| for item in extra_items: |
| log.critical(item) |
| if len(flattend_errors) > 0: |
| log.critical('Bad Values: ') |
| for err in flattend_errors: |
| log.critical(err) |
| return False |
| |
| @classmethod |
| def _convert_configspec_to_str_list(cls, configspec): |
| """ |
| Check the configspec for sanity. |
| |
| @param configspec: config spec of type str or list or Section |
| |
| @return: config spec of type list |
| |
| """ |
| if isinstance(configspec, list): |
| return configspec |
| if isinstance(configspec, configobj.ConfigObj): |
| return configspec.write() |
| else: |
| raise ConfigurationError( |
| 'Configuration objects require ' + |
| 'self.configspec to be of type str or list, not %s' % |
| type(configspec)) |
| |
| @classmethod |
| def get_default_config(cls): |
| """ |
| @return: a ConfigObj with the default values filled in. |
| |
| """ |
| configspec = cls._convert_configspec_to_str_list(cls.CONFIGSPEC) |
| try: |
| config = configobj.ConfigObj(configspec=configspec) |
| validator = validate.Validator() |
| config.validate(validator, copy=True) |
| # Other parts of the class expect this to return a ConfigObj |
| assert isinstance(config, configobj.ConfigObj) |
| except Exception as e: |
| # Explicitly catch all types of errors. Any error raised |
| # by the above code means the config was bad, and we should |
| # stop and print out useful debugging info. |
| pretty_str = pprint.pformat(configspec) |
| log.critical('This configspec failed: \n%s ' % pretty_str) |
| log.exception(e) |
| raise ConfigurationError(e.message) |
| return config |