VirtualMailManager/config.py
branchv0.7.x
changeset 643 df1e3b67882a
parent 642 4cd9d0a9f42f
child 650 429ba58bc302
equal deleted inserted replaced
642:4cd9d0a9f42f 643:df1e3b67882a
     6     ~~~~~~~~~~~~~~~~~~~~~~~~~
     6     ~~~~~~~~~~~~~~~~~~~~~~~~~
     7 
     7 
     8     VMM's configuration module for simplified configuration access.
     8     VMM's configuration module for simplified configuration access.
     9 """
     9 """
    10 
    10 
    11 from ConfigParser import \
    11 from configparser import \
    12      Error, MissingSectionHeaderError, NoOptionError, NoSectionError, \
    12      Error, MissingSectionHeaderError, NoOptionError, NoSectionError, \
    13      ParsingError, RawConfigParser
    13      ParsingError, RawConfigParser
    14 from cStringIO import StringIO
    14 from io import StringIO
    15 
    15 
    16 from VirtualMailManager.common import VERSION_RE, \
    16 from VirtualMailManager.common import VERSION_RE, \
    17      exec_ok, expand_path, get_unicode, lisdir, size_in_bytes, version_hex
    17      exec_ok, expand_path, get_unicode, lisdir, size_in_bytes, version_hex
    18 from VirtualMailManager.constants import CONF_ERROR
    18 from VirtualMailManager.constants import CONF_ERROR
    19 from VirtualMailManager.errors import ConfigError, VMMError
    19 from VirtualMailManager.errors import ConfigError, VMMError
    20 from VirtualMailManager.maillocation import known_format
    20 from VirtualMailManager.maillocation import known_format
    21 from VirtualMailManager.password import verify_scheme as _verify_scheme
    21 from VirtualMailManager.password import verify_scheme as _verify_scheme
       
    22 import collections
    22 
    23 
    23 DB_MODULES = ('psycopg2', 'pypgsql')
    24 DB_MODULES = ('psycopg2', 'pypgsql')
    24 DB_SSL_MODES = ('allow', 'disabled', 'prefer', 'require', 'verify-ca',
    25 DB_SSL_MODES = ('allow', 'disabled', 'prefer', 'require', 'verify-ca',
    25                 'verify-full')
    26                 'verify-full')
    26 
    27 
    84         if isinstance(value, bool):
    85         if isinstance(value, bool):
    85             return value
    86             return value
    86         if value.lower() in self._boolean_states:
    87         if value.lower() in self._boolean_states:
    87             return self._boolean_states[value.lower()]
    88             return self._boolean_states[value.lower()]
    88         else:
    89         else:
    89             raise ConfigValueError(_(u"Not a boolean: '%s'") %
    90             raise ConfigValueError(_("Not a boolean: '%s'") %
    90                                    get_unicode(value))
    91                                    get_unicode(value))
    91 
    92 
    92     def getboolean(self, section, option):
    93     def getboolean(self, section, option):
    93         """Returns the boolean value of the option, in the given
    94         """Returns the boolean value of the option, in the given
    94         section.
    95         section.
   122           * `NoOptionError`
   123           * `NoOptionError`
   123         """
   124         """
   124         sect_opt = section_option.lower().split('.')
   125         sect_opt = section_option.lower().split('.')
   125         # TODO: cache it
   126         # TODO: cache it
   126         if len(sect_opt) != 2 or not sect_opt[0] or not sect_opt[1]:
   127         if len(sect_opt) != 2 or not sect_opt[0] or not sect_opt[1]:
   127             raise BadOptionError(_(u"Bad format: '%s' - expected: "
   128             raise BadOptionError(_("Bad format: '%s' - expected: "
   128                                    u"section.option") %
   129                                    "section.option") %
   129                                  get_unicode(section_option))
   130                                  get_unicode(section_option))
   130         if not sect_opt[0] in self._cfg:
   131         if not sect_opt[0] in self._cfg:
   131             raise NoSectionError(sect_opt[0])
   132             raise NoSectionError(sect_opt[0])
   132         if not sect_opt[1] in self._cfg[sect_opt[0]]:
   133         if not sect_opt[1] in self._cfg[sect_opt[0]]:
   133             raise NoOptionError(sect_opt[1], sect_opt[0])
   134             raise NoOptionError(sect_opt[1], sect_opt[0])
   141             sect = self._sections[section]
   142             sect = self._sections[section]
   142         elif not section in self._cfg:
   143         elif not section in self._cfg:
   143             raise NoSectionError(section)
   144             raise NoSectionError(section)
   144         else:
   145         else:
   145             return ((k, self._cfg[section][k].default)
   146             return ((k, self._cfg[section][k].default)
   146                     for k in self._cfg[section].iterkeys())
   147                     for k in self._cfg[section].keys())
   147         # still here? Get defaults and merge defaults with configured setting
   148         # still here? Get defaults and merge defaults with configured setting
   148         defaults = dict((k, self._cfg[section][k].default)
   149         defaults = dict((k, self._cfg[section][k].default)
   149                         for k in self._cfg[section].iterkeys())
   150                         for k in self._cfg[section].keys())
   150         defaults.update(sect)
   151         defaults.update(sect)
   151         if '__name__' in defaults:
   152         if '__name__' in defaults:
   152             del defaults['__name__']
   153             del defaults['__name__']
   153         return defaults.iteritems()
   154         return iter(defaults.items())
   154 
   155 
   155     def dget(self, option):
   156     def dget(self, option):
   156         """Returns the value of the `option`.
   157         """Returns the value of the `option`.
   157 
   158 
   158         If the option could not be found in the configuration file, the
   159         If the option could not be found in the configuration file, the
   214         except(BadOptionError, NoSectionError, NoOptionError):
   215         except(BadOptionError, NoSectionError, NoOptionError):
   215             return False
   216             return False
   216 
   217 
   217     def sections(self):
   218     def sections(self):
   218         """Returns an iterator object for all configuration sections."""
   219         """Returns an iterator object for all configuration sections."""
   219         return self._cfg.iterkeys()
   220         return iter(self._cfg.keys())
   220 
   221 
   221 
   222 
   222 class LazyConfigOption(object):
   223 class LazyConfigOption(object):
   223     """A simple container class for configuration settings.
   224     """A simple container class for configuration settings.
   224 
   225 
   245           None or any method, that takes one argument, in order to
   246           None or any method, that takes one argument, in order to
   246           check the value, when `LazyConfig.set()` is called.
   247           check the value, when `LazyConfig.set()` is called.
   247         """
   248         """
   248         self.__cls = cls
   249         self.__cls = cls
   249         self.__default = default if default is None else self.__cls(default)
   250         self.__default = default if default is None else self.__cls(default)
   250         if not callable(getter):
   251         if not isinstance(getter, collections.Callable):
   251             raise TypeError('getter has to be a callable, got a %r' %
   252             raise TypeError('getter has to be a callable, got a %r' %
   252                             getter.__class__.__name__)
   253                             getter.__class__.__name__)
   253         self.__getter = getter
   254         self.__getter = getter
   254         if validate and not callable(validate):
   255         if validate and not isinstance(validate, collections.Callable):
   255             raise TypeError('validate has to be callable or None, got a %r' %
   256             raise TypeError('validate has to be callable or None, got a %r' %
   256                             validate.__class__.__name__)
   257                             validate.__class__.__name__)
   257         self.__validate = validate
   258         self.__validate = validate
   258 
   259 
   259     @property
   260     @property
   332                 'quota_messages': LCO(int, 0, self.getint),
   333                 'quota_messages': LCO(int, 0, self.getint),
   333                 'transport': LCO(str, 'dovecot:', self.get),
   334                 'transport': LCO(str, 'dovecot:', self.get),
   334             },
   335             },
   335             'mailbox': {
   336             'mailbox': {
   336                 'folders': LCO(str, 'Drafts:Sent:Templates:Trash',
   337                 'folders': LCO(str, 'Drafts:Sent:Templates:Trash',
   337                                self.unicode),
   338                                self.str),
   338                 'format': LCO(str, 'maildir', self.get, check_mailbox_format),
   339                 'format': LCO(str, 'maildir', self.get, check_mailbox_format),
   339                 'root': LCO(str, 'Maildir', self.unicode),
   340                 'root': LCO(str, 'Maildir', self.str),
   340                 'subscribe': LCO(bool_t, True, self.getboolean),
   341                 'subscribe': LCO(bool_t, True, self.getboolean),
   341             },
   342             },
   342             'misc': {
   343             'misc': {
   343                 'base_directory': LCO(str, '/srv/mail', self.get, is_dir),
   344                 'base_directory': LCO(str, '/srv/mail', self.get, is_dir),
   344                 'crypt_blowfish_rounds': LCO(int, 5, self.getint),
   345                 'crypt_blowfish_rounds': LCO(int, 5, self.getint),
   358         invalid.
   359         invalid.
   359         """
   360         """
   360         with open(self._cfg_filename, 'r') as self._cfg_file:
   361         with open(self._cfg_filename, 'r') as self._cfg_file:
   361             try:
   362             try:
   362                 self.readfp(self._cfg_file)
   363                 self.readfp(self._cfg_file)
   363             except (MissingSectionHeaderError, ParsingError), err:
   364             except (MissingSectionHeaderError, ParsingError) as err:
   364                 raise ConfigError(str(err), CONF_ERROR)
   365                 raise ConfigError(str(err), CONF_ERROR)
   365 
   366 
   366     def check(self):
   367     def check(self):
   367         """Performs a configuration check.
   368         """Performs a configuration check.
   368 
   369 
   369         Raises a ConfigError if settings w/o a default value are missed.
   370         Raises a ConfigError if settings w/o a default value are missed.
   370         Or some settings have a invalid value.
   371         Or some settings have a invalid value.
   371         """
   372         """
   372         def iter_dict():
   373         def iter_dict():
   373             for section, options in self._missing.iteritems():
   374             for section, options in self._missing.items():
   374                 errmsg.write(_(u'* Section: %s\n') % section)
   375                 errmsg.write(_('* Section: %s\n') % section)
   375                 errmsg.writelines(u'    %s\n' % option for option in options)
   376                 errmsg.writelines('    %s\n' % option for option in options)
   376             self._missing.clear()
   377             self._missing.clear()
   377 
   378 
   378         errmsg = None
   379         errmsg = None
   379         self._chk_non_default()
   380         self._chk_non_default()
   380         miss_vers = 'misc' in self._missing and \
   381         miss_vers = 'misc' in self._missing and \
   381                     'dovecot_version' in self._missing['misc']
   382                     'dovecot_version' in self._missing['misc']
   382         if self._missing:
   383         if self._missing:
   383             errmsg = StringIO()
   384             errmsg = StringIO()
   384             errmsg.write(_(u'Check of configuration file %s failed.\n') %
   385             errmsg.write(_('Check of configuration file %s failed.\n') %
   385                          self._cfg_filename)
   386                          self._cfg_filename)
   386             errmsg.write(_(u'Missing options, which have no default value.\n'))
   387             errmsg.write(_('Missing options, which have no default value.\n'))
   387             iter_dict()
   388             iter_dict()
   388         self._chk_possible_values(miss_vers)
   389         self._chk_possible_values(miss_vers)
   389         if self._missing:
   390         if self._missing:
   390             if not errmsg:
   391             if not errmsg:
   391                 errmsg = StringIO()
   392                 errmsg = StringIO()
   392                 errmsg.write(_(u'Check of configuration file %s failed.\n') %
   393                 errmsg.write(_('Check of configuration file %s failed.\n') %
   393                              self._cfg_filename)
   394                              self._cfg_filename)
   394                 errmsg.write(_(u'Invalid configuration values.\n'))
   395                 errmsg.write(_('Invalid configuration values.\n'))
   395             else:
   396             else:
   396                 errmsg.write('\n' + _(u'Invalid configuration values.\n'))
   397                 errmsg.write('\n' + _('Invalid configuration values.\n'))
   397             iter_dict()
   398             iter_dict()
   398         if errmsg:
   399         if errmsg:
   399             raise ConfigError(errmsg.getvalue(), CONF_ERROR)
   400             raise ConfigError(errmsg.getvalue(), CONF_ERROR)
   400 
   401 
   401     def hexversion(self, section, option):
   402     def hexversion(self, section, option):
   406     def get_in_bytes(self, section, option):
   407     def get_in_bytes(self, section, option):
   407         """Converts the size value (e.g.: 1024k) from the *option*'s
   408         """Converts the size value (e.g.: 1024k) from the *option*'s
   408         value to a long"""
   409         value to a long"""
   409         return size_in_bytes(self.get(section, option))
   410         return size_in_bytes(self.get(section, option))
   410 
   411 
   411     def unicode(self, section, option):
   412     def str(self, section, option):
   412         """Returns the value of the `option` from `section`, converted
   413         """Returns the value of the `option` from `section`, converted
   413         to Unicode."""
   414         to Unicode."""
   414         return get_unicode(self.get(section, option))
   415         return get_unicode(self.get(section, option))
   415 
   416 
   416     def _chk_non_default(self):
   417     def _chk_non_default(self):
   417         """Checks all section's options for settings w/o a default
   418         """Checks all section's options for settings w/o a default
   418         value. Missing items will be stored in _missing.
   419         value. Missing items will be stored in _missing.
   419         """
   420         """
   420         for section in self._cfg.iterkeys():
   421         for section in self._cfg.keys():
   421             missing = []
   422             missing = []
   422             for option, value in self._cfg[section].iteritems():
   423             for option, value in self._cfg[section].items():
   423                 if (value.default is None and
   424                 if (value.default is None and
   424                     not RawConfigParser.has_option(self, section, option)):
   425                     not RawConfigParser.has_option(self, section, option)):
   425                     missing.append(option)
   426                     missing.append(option)
   426             if missing:
   427             if missing:
   427                 self._missing[section] = missing
   428                 self._missing[section] = missing
   430         """Check settings for which the possible values are known."""
   431         """Check settings for which the possible values are known."""
   431         if not miss_vers:
   432         if not miss_vers:
   432             value = self.get('misc', 'dovecot_version')
   433             value = self.get('misc', 'dovecot_version')
   433             if not VERSION_RE.match(value):
   434             if not VERSION_RE.match(value):
   434                 self._missing['misc'] = ['version: ' +
   435                 self._missing['misc'] = ['version: ' +
   435                         _(u"Not a valid Dovecot version: '%s'") % value]
   436                         _("Not a valid Dovecot version: '%s'") % value]
   436         # section database
   437         # section database
   437         db_err = []
   438         db_err = []
   438         value = self.dget('database.module').lower()
   439         value = self.dget('database.module').lower()
   439         if value not in DB_MODULES:
   440         if value not in DB_MODULES:
   440             db_err.append('module: ' +
   441             db_err.append('module: ' +
   441                           _(u"Unsupported database module: '%s'") % value)
   442                           _("Unsupported database module: '%s'") % value)
   442         if value == 'psycopg2':
   443         if value == 'psycopg2':
   443             value = self.dget('database.sslmode')
   444             value = self.dget('database.sslmode')
   444             if value not in DB_SSL_MODES:
   445             if value not in DB_SSL_MODES:
   445                 db_err.append('sslmode: ' +
   446                 db_err.append('sslmode: ' +
   446                               _(u"Unknown pgsql SSL mode: '%s'") % value)
   447                               _("Unknown pgsql SSL mode: '%s'") % value)
   447         if db_err:
   448         if db_err:
   448             self._missing['database'] = db_err
   449             self._missing['database'] = db_err
   449         # section mailbox
   450         # section mailbox
   450         value = self.dget('mailbox.format')
   451         value = self.dget('mailbox.format')
   451         if not known_format(value):
   452         if not known_format(value):
   452             self._missing['mailbox'] = ['format: ' +
   453             self._missing['mailbox'] = ['format: ' +
   453                               _(u"Unsupported mailbox format: '%s'") % value]
   454                               _("Unsupported mailbox format: '%s'") % value]
   454         # section domain
   455         # section domain
   455         try:
   456         try:
   456             value = self.dget('domain.quota_bytes')
   457             value = self.dget('domain.quota_bytes')
   457         except (ValueError, TypeError), err:
   458         except (ValueError, TypeError) as err:
   458             self._missing['domain'] = [u'quota_bytes: ' + str(err)]
   459             self._missing['domain'] = ['quota_bytes: ' + str(err)]
   459 
   460 
   460 
   461 
   461 def is_dir(path):
   462 def is_dir(path):
   462     """Check if the expanded path is a directory.  When the expanded path
   463     """Check if the expanded path is a directory.  When the expanded path
   463     is a directory the expanded path will be returned.  Otherwise a
   464     is a directory the expanded path will be returned.  Otherwise a
   464     ConfigValueError will be raised.
   465     ConfigValueError will be raised.
   465     """
   466     """
   466     path = expand_path(path)
   467     path = expand_path(path)
   467     if lisdir(path):
   468     if lisdir(path):
   468         return path
   469         return path
   469     raise ConfigValueError(_(u"No such directory: %s") % get_unicode(path))
   470     raise ConfigValueError(_("No such directory: %s") % get_unicode(path))
   470 
   471 
   471 
   472 
   472 def check_db_module(module):
   473 def check_db_module(module):
   473     """Check if the *module* is a supported pgsql module."""
   474     """Check if the *module* is a supported pgsql module."""
   474     if module.lower() in DB_MODULES:
   475     if module.lower() in DB_MODULES:
   475         return module
   476         return module
   476     raise ConfigValueError(_(u"Unsupported database module: '%s'") %
   477     raise ConfigValueError(_("Unsupported database module: '%s'") %
   477                            get_unicode(module))
   478                            get_unicode(module))
   478 
   479 
   479 
   480 
   480 def check_db_ssl_mode(ssl_mode):
   481 def check_db_ssl_mode(ssl_mode):
   481     """Check if the *ssl_mode* is one of the SSL modes, known by pgsql."""
   482     """Check if the *ssl_mode* is one of the SSL modes, known by pgsql."""
   482     if ssl_mode in DB_SSL_MODES:
   483     if ssl_mode in DB_SSL_MODES:
   483         return ssl_mode
   484         return ssl_mode
   484     raise ConfigValueError(_(u"Unknown pgsql SSL mode: '%s'") %
   485     raise ConfigValueError(_("Unknown pgsql SSL mode: '%s'") %
   485                            get_unicode(ssl_mode))
   486                            get_unicode(ssl_mode))
   486 
   487 
   487 
   488 
   488 def check_mailbox_format(format):
   489 def check_mailbox_format(format):
   489     """
   490     """
   492     be raised.
   493     be raised.
   493     """
   494     """
   494     format = format.lower()
   495     format = format.lower()
   495     if known_format(format):
   496     if known_format(format):
   496         return format
   497         return format
   497     raise ConfigValueError(_(u"Unsupported mailbox format: '%s'") %
   498     raise ConfigValueError(_("Unsupported mailbox format: '%s'") %
   498                            get_unicode(format))
   499                            get_unicode(format))
   499 
   500 
   500 
   501 
   501 def check_size_value(value):
   502 def check_size_value(value):
   502     """Check if the size value *value* has the proper format, e.g.: 1024k.
   503     """Check if the size value *value* has the proper format, e.g.: 1024k.
   503     Returns the validated value string if it has the expected format.
   504     Returns the validated value string if it has the expected format.
   504     Otherwise a `ConfigValueError` will be raised."""
   505     Otherwise a `ConfigValueError` will be raised."""
   505     try:
   506     try:
   506         tmp = size_in_bytes(value)
   507         tmp = size_in_bytes(value)
   507     except (TypeError, ValueError), err:
   508     except (TypeError, ValueError) as err:
   508         raise ConfigValueError(_(u"Not a valid size value: '%s'") %
   509         raise ConfigValueError(_("Not a valid size value: '%s'") %
   509                                get_unicode(value))
   510                                get_unicode(value))
   510     return value
   511     return value
   511 
   512 
   512 
   513 
   513 def check_version_format(version_string):
   514 def check_version_format(version_string):
   514     """Check if the *version_string* has the proper format, e.g.: '1.2.3'.
   515     """Check if the *version_string* has the proper format, e.g.: '1.2.3'.
   515     Returns the validated version string if it has the expected format.
   516     Returns the validated version string if it has the expected format.
   516     Otherwise a `ConfigValueError` will be raised.
   517     Otherwise a `ConfigValueError` will be raised.
   517     """
   518     """
   518     if not VERSION_RE.match(version_string):
   519     if not VERSION_RE.match(version_string):
   519         raise ConfigValueError(_(u"Not a valid Dovecot version: '%s'") %
   520         raise ConfigValueError(_("Not a valid Dovecot version: '%s'") %
   520                                get_unicode(version_string))
   521                                get_unicode(version_string))
   521     return version_string
   522     return version_string
   522 
   523 
   523 
   524 
   524 def verify_scheme(scheme):
   525 def verify_scheme(scheme):
   525     """Checks if the password scheme *scheme* can be accepted and returns
   526     """Checks if the password scheme *scheme* can be accepted and returns
   526     the verified scheme.
   527     the verified scheme.
   527     """
   528     """
   528     try:
   529     try:
   529         scheme, encoding = _verify_scheme(scheme)
   530         scheme, encoding = _verify_scheme(scheme)
   530     except VMMError, err:  # 'cast' it
   531     except VMMError as err:  # 'cast' it
   531         raise ConfigValueError(err.msg)
   532         raise ConfigValueError(err.msg)
   532     if not encoding:
   533     if not encoding:
   533         return scheme
   534         return scheme
   534     return '%s.%s' % (scheme, encoding)
   535     return '%s.%s' % (scheme, encoding)
   535 
   536