blob: 4e61614c0a493cea85ce5e03f2f0b68fac48f9bf [file] [log] [blame]
# 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