Package Products :: Package ZenUtils :: Module config |
[hide private]
[frames] | no frames]

# Source Code for Module Products.ZenUtils.config

      1  ###########################################################################
      2  #
      3  # This program is part of Zenoss Core, an open source monitoring platform.
      4  # Copyright (C) 2010, Zenoss Inc.
      5  #
      6  # This program is free software; you can redistribute it and/or modify it
      7  # under the terms of the GNU General Public License version 2 or (at your
      8  # option) any later version as published by the Free Software Foundation.
      9  #
     10  # For complete information please visit: http://www.zenoss.com/oss/
     11  #
     12  ###########################################################################
     13
     14  __doc__ = """
     15  Zenoss config parsers.
     16
     17  There are mutiple stages to config parsing. Parsing is split into stages so that
     18  we can validate a whole config file and possibly rebuild it to correct errors.
     19
     20  The stages are:
     21
     22  * Parse - Split the config file in to ConfigLine types while maintaining line order, comments, and new lines
     23  * Validate - Check that all lines are valid
     24  * Report - Investigate why a line might be invalid (ex: invalid key format)
     25  * Load - Get a config object back
     26  * Write - An optional stage to write the config back to a file
     27  """
     28
     29  import re
     30
     31 -class ConfigError(Exception):
     32      """
     33      Error for problems parsing config files.
     34      """
     35      pass
     36
     37 -class ConfigLineError(ConfigError):
     38      """
     39      Error for problems parsing config files with line context.
     40      """
     41 -    def __init__(self, message, lineno):
     42          super(ConfigLineError, self).__init__(message)
     43          self.lineno = lineno
     44
     45 -    def __str__(self):
     46          return '%s on line %d' % (self.message, self.lineno)
     47
     48 -class ConfigErrors(ConfigError):
     49      """
     50      A group of errors while parsing config.
     51      """
     52 -    def __init__(self, message, errors):
     53          super(ConfigErrors, self).__init__(message)
     54          self.errors = errors
     55
     56 -    def __str__(self):
     57          output = [self.message]
     58          for error in self.errors:
     59              output.append(str(error))
     60
     61          return '\n    - '.join(output)
     62
     63 -class InvalidKey(ConfigError):
     64      pass
     65
     66 -class ConfigLineKeyError(ConfigLineError):
     67      pass
     68
     69 -class Config(dict):
     70      """
     71      A bunch of configuration settings. Uses dictionary access,
     72      or object attribute access.
     73
     74      Provides some Convenience functions for different types.
     75      """
     76 -    def __getattr__(self, attr):
     77          return self[attr]
     78
     79 -    def getbool(self, key, default=None):
     80          """
     81          Convenience function to convert the value to a bool.
     82          Valid values are and case of true, yes, y, 1 anything
     83          else is considered False.
     84
     85          If key doesn't exist, returns `default`.
     86          """
     87          try:
     88              return self[key].lower() in ('true', 'yes', 'y', '1')
     89          except KeyError:
     90              return default
     91
     92      getboolean = getbool
     93
     94 -    def getint(self, key, default=None):
     95          """
     96          Convenience function to convert the value to an int.
     97          Valid values are anything `int(x)` will accept.
     98
     99          If key doesn't exist or can't be converted to int, returns `default`.
    100          """
    101          try:
    102              return int(self[key])
    103          except (KeyError, ValueError):
    104              return default
    105
    106 -    def getfloat(self, key, default=None):
    107          """
    108          Convenience function to convert the value to a float.
    109          Valid values are anything `float(x)` will accept.
    110
    111          If key doesn't exist or can't be converted to float, returns `default`.
    112          """
    113          try:
    114              return float(self[key])
    115          except (KeyError, ValueError):
    116              return default
    117
    118 -class ConfigLine(object):
    119      """
    120      Abstract class that represents a single line in the config.
    121      """
    122 -    def __init__(self, line):
    123          self.line = line
    124
    125 -    def __str__(self):
    126          return self.line
    127
    128      @property
    129 -    def setting(self):
    130          """
    131          Return a key, value tuple if this line represents a setting.
    132          Implemented in base classes.
    133          """
    134          return None
    135
    136      @classmethod
    137 -    def parse(cls, line):
    138          """
    139          Returns an instance of cls if this class can parse this line. Otherwise returns None.
    140          Implemented in base classes.
    141          """
    142          return None
    143
    144      @classmethod
    145 -    def checkError(cls, line, lineno):
    146          """
    147          Checks the string for possible matches, considers why it doesn't match exactly if it's close
    148          and returns a ConfigLineError.
    149          Implemented in base classes.
    150          """
    151          return None
    152
    153 -class SettingLine(ConfigLine):
    154      """
    155      Represents a config line with a `key = value` pair.
    156      """
    157      _regexp = re.compile(r'^(?P<key>[a-z][a-z\d_]*)\s*(?P<delim>(=|:|\s)+)\s*(?P<value>.+)$', re.I)
    158
    159 -    def __init__(self, key, value=None, delim='='):
    160          self.key = key
    161          self.value = value
    162          self.delim = delim
    163
    164      @property
    165 -    def setting(self):
    166          return self.key, self.value
    167
    168 -    def __str__(self):
    169          return '{key} {delim} {value}'.format(**self.__dict__)
    170
    171      @classmethod
    172 -    def checkError(cls, line, lineno):
    173          match = re.match(r'^(?P<key>.+?)\s*(?P<delim>(=|:|\s)+)\s*(?P<value>.+)$', line, re.I)
    174          if match and not cls._regexp.match(line):
    175              return ConfigLineKeyError('Invalid key "%s"' % match.groupdict()['key'], lineno)
    176
    177
    178      @classmethod
    179 -    def parse(cls, line):
    180          match = cls._regexp.match(line)
    181          if match:
    182              data = match.groupdict()
    183              return cls(**data)
    184
    185 -class CommentLine(ConfigLine):
    186      @classmethod
    187 -    def parse(cls, line):
    188          if line.startswith('#'):
    189              return cls(line[1:].strip())
    190
    191 -    def __str__(self):
    192          return '# %s' % self.line
    193
    194 -class EmptyLine(ConfigLine):
    195 -    def __init__(self):
    196          pass
    197
    198      @classmethod
    199 -    def parse(cls, line):
    200          if line == '':
    201              return cls()
    202
    203 -    def __str__(self):
    204          return ''
    205
    206 -class InvalidLine(ConfigLine):
    207      """
    208      Default line if no other ConfigLines matched. Assumed to be invalid
    209      input.
    210      """
    211      pass
    212
    213 -class ConfigFile(object):
    214      """
    215      Parses Zenoss's config file format.
    216
    217      Example:
    218
    219          key value
    220          key intvalue
    221          key = value
    222          key=value
    223          key:value
    224          key : value
    225      """
    226
    227      # Lines to parse the config against
    228      _lineTypes = [
    229          SettingLine,
    230          CommentLine,
    231          EmptyLine,
    232      ]
    233
    234      # Line to use if the line didn't match any other types
    235      _invalidLineType = InvalidLine
    236
    237 -    def __init__(self, file):
    238          """
    239          @param file file-like-object
    240          """
    241          self.file = file
    242          self.filename = self.file.name if hasattr(self.file, 'name') else 'Unknown'
    243          self._lines = None
    244
    245 -    def _parseLine(self, line):
    246          cleanedLine = line.strip()
    247          for type in self._lineTypes:
    248              match = type.parse(cleanedLine)
    249              if match:
    250                  return match
    251
    252          return self._invalidLineType(cleanedLine)
    253
    254
    255 -    def _checkLine(self, line, lineno):
    256          cleanedLine = line.strip()
    257          for type in self._lineTypes:
    258              match = type.checkError(cleanedLine, lineno)
    259              if match:
    260                  return match
    261
    262 -    def parse(self):
    263          """
    264          Parse a config file which has key-value pairs.Returns a list of config
    265          line information. This line information can be used to accuratly recreate
    266          the config without losing comments or invalid data.
    267          """
    268          if self._lines is None:
    269              self._lines = []
    270              for line in self.file:
    271                  self._lines.append(self._parseLine(line))
    272
    273          return self._lines
    274
    275 -    def write(self, file):
    276          """
    277          Write the config out to a file. Takes a new file argument
    278          because the input file object often doesn't have write access.
    279
    280          @param file file-like-object
    281          """
    282          for line in self:
    283              file.write(str(line) + '\n')
    284
    285 -    def validate(self):
    286          """
    287          Validate that there are no errors in the config file
    288
    289          @throws ConfigError
    290          """
    291          errors = []
    292          for lineno, line in enumerate(self):
    293              if isinstance(line, self._invalidLineType):
    294                  # Identify possible errors
    295                  error = self._checkLine(line.line, lineno)
    296                  if error:
    297                      errors.append(error)
    298                  else:
    299                      errors.append(ConfigLineError('Unexpected config line "%s"' % line.line, lineno + 1))
    300
    301          if errors:
    302              raise ConfigErrors('There were errors parsing the config "%s".' % self.filename, errors)
    303
    304 -    def __iter__(self):
    305          for line in self.parse():
    306              yield line
    307
    308 -    def items(self):
    309          for line in self:
    310              if line.setting:
    311                  yield line.setting
    312
    313 -class Parser(object):
    314 -    def __call__(self, file):
    315          configFile = ConfigFile(file)
    316          configFile.validate()
    317          return configFile.items()
    318
    319
    320 -class ConfigLoader(object):
    321      """
    322      Lazily load the config when requested.
    323      """
    324 -    def __init__(self, config_files, config=Config, parser=Parser()):
    325          """
    326          @param config Config The config instance or class to load data into. Must support update which accepts an iterable of (key, value).
    327          @param parser Parser The parser to use to parse the config files. Must be a callable and return an iterable of (key, value).
    328          @param config_files list<string> A list of config file names to parse in order.
    329          """
    330          if not isinstance(config_files, list):
    331              config_files = [config_files]
    332
    333          self.config_files = config_files
    334          self.parser = parser
    335          self.config = config
    336          self._config = None
    337
    338 -    def load(self):
    339          """
    340          Load the config_files into an instance of config_class
    341          """
    342          if isinstance(self.config, type):
    343              self._config = self.config()
    344          else:
    345              self._config = self.config
    346
    347          if not self.config_files:
    348              raise ConfigError('Config loader has no config files to load.')
    349
    350          for file in self.config_files:
    351              if not hasattr(file, 'read') and isinstance(file, basestring):
    352                  # Look like a file name, open it
    353                  with open(file, 'r') as fp:
    354                      options = self.parser(fp)
    355              else:
    356                  options = self.parser(file)
    357
    358              self._config.update(options)
    359
    360 -    def __call__(self):
    361          """
    362          Lazily load the config file.
    363          """
    364          if self._config is None:
    365              self.load()
    366
    367          return self._config
    368