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

# Source Code for Module Products.ZenUtils.config

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