diff -r c0e2c7687dd3 -r 974bafa59330 VirtualMailManager/Config.py --- a/VirtualMailManager/Config.py Fri Jan 22 04:31:38 2010 +0000 +++ b/VirtualMailManager/Config.py Sun Jan 24 06:40:38 2010 +0000 @@ -2,73 +2,326 @@ # Copyright (c) 2007 - 2010, Pascal Volk # See COPYING for distribution information. -"""Configuration class for read, modify and write the -configuration from Virtual Mail Manager. +"""vmm's configuration module for simplified configuration access. + +This module defines a few classes: + +``LazyConfig`` + This class provides the following additonal methods + * `LazyConfig.pget()` + polymorphic getter which returns the value with the appropriate + type. + * `LazyConfig.dget()` + like *pget()*, but checks additonal for default values in + `LazyConfig._cfg`. + * `LazyConfig.set()` + like `RawConfigParser.set()`, but converts the new value to the + appropriate type/class and optional validates the new value. + * `LazyConfig.bool_new()` + converts data from raw_input into boolean values. + * `LazyConfig.get_boolean()` + like `RawConfigParser.getboolean()`, but doesn't fail on real + `bool` values. + +``Config`` + The Config class used by vmm. + +``LazyConfigOption`` + The class for the configuration objects in the ``Config`` class' + ``_cfg`` dictionary. """ + from shutil import copy2 -from ConfigParser import ConfigParser, MissingSectionHeaderError, ParsingError +from ConfigParser import (Error, MissingSectionHeaderError, NoOptionError, + NoSectionError, ParsingError, RawConfigParser) from cStringIO import StringIO -from __main__ import ENCODING, ERR, w_std +from __main__ import os, ENCODING, ERR, get_unicode, w_std from Exceptions import VMMConfigException -class Config(ConfigParser): + +class BadOptionError(Error): + """Raised when a option isn't in the format 'section.option'.""" + pass + + +class ConfigValueError(Error): + """Raised when creating or validating of new values fails.""" + pass + + +class NoDefaultError(Error): + """Raised when the requested option has no default value.""" + + def __init__(self, section, option): + Error.__init__(self, 'Option %r in section %r has no default value' %( + option, section)) + + +class LazyConfig(RawConfigParser): + """The **lazy** derivate of the `RawConfigParser`. + + There are two additional getters: + + `LazyConfig.pget()` + The polymorphic getter, which returns a option's value with the + appropriate type. + `LazyConfig.dget()` + Like `LazyConfig.pget()`, but returns the option's default, from + `LazyConfig._cfg['sectionname']['optionname'].default`, if the + option is not configured in a ini-like configuration file. + + + `LazyConfig.set()` differs from ``RawConfigParser``'s ``set()`` method. + ``LazyConfig.set()`` takes the ``section`` and ``option`` arguments + combined to a single string in the form + "``section``\ **.**\ ``option``". + """ + def __init__(self): + RawConfigParser.__init__(self) + self._modified = False + self._cfg = { + 'sectionname': { + 'optionname': LazyConfigOption(int, 1, self.getint) + } + } + """sample _cfg dictionary. Create your own in your derived class.""" + + def bool_new(self, value): + """Converts the string `value` into a `bool` and returns it. + + | '1', 'on', 'yes' and 'true' will become ``True`` + | '0', 'off', 'no' and 'false' will become ``False`` + + Throws a `ConfigValueError` for all other values, except ``bool``\ s. + """ + if isinstance(value, bool): + return value + if value.lower() in self._boolean_states: + return self._boolean_states[value.lower()] + else: + raise ConfigValueError(_(u'Not a boolean: “%s”') % \ + get_unicode(value)) + + def get_boolean(self, section, option): + # if the setting was not written to the configuration file, it may + # be still a boolean value - lets see + if self._modified: + tmp = self.get(section, option) + if isinstance(tmp, bool): + return tmp + return self.getboolean(section, option) + + def __get_section_option(self, section_option): + """splits ``section_option`` (section\ **.**\ option) in two parts + and returns them as list ``[section, option]``, if: + + * it likes the format of ``section_option`` + * the ``section`` is known + * the ``option`` is known + + Else one of the following exceptions will be thrown: + + * `BadOptionError` + * `NoSectionError` + * `NoOptionError` + """ + sect_opt = section_option.lower().split('.') + if len(sect_opt) != 2:# do we need a regexp to check the format? + raise BadOptionError( + _(u'Bad format: “%s” - expected: section.option') % \ + get_unicode(section_option)) + if not sect_opt[0] in self._cfg: + raise NoSectionError(sect_opt[0]) + if not sect_opt[1] in self._cfg[sect_opt[0]]: + raise NoOptionError(sect_opt[1], sect_opt[0]) + return sect_opt + + def items(self, section): + """returns a ``list`` with key, value ``tuples`` from the given + ``section``: ``[(key, value), …]``""" + if section in self._sections:# check if the section was parsed + d2 = self._sections[section] + elif not section in self._cfg: + raise NoSectionError(section) + else: + return ((k, self._cfg[section][k].default) \ + for k in self._cfg[section].iterkeys()) + # still here? Get defaults and merge defaults with configured setting + d = dict((k, self._cfg[section][k].default) \ + for k in self._cfg[section].iterkeys()) + d.update(d2) + if '__name__' in d: del d['__name__'] + return d.iteritems() + + def dget(self, option): + """Returns the value of the `option`. + + If the option could not be found in the configuration file, the + configured default value, from ``LazyConfig._cfg`` will be + returned. + + Arguments: + + `option` : string + the configuration option in the form + "``section``\ **.**\ ``option``" + + Throws a `NoDefaultError`, if no default value was passed to + `LazyConfigOption.__init__()` for the `option`. + """ + section, option = self.__get_section_option(option) + try: + return self._cfg[section][option].getter(section, option) + except (NoSectionError, NoOptionError): + if not self._cfg[section][option].default is None: + return self._cfg[section][option].default + else: + raise NoDefaultError(section, option) + + def pget(self, option): + """Returns the value of the `option`.""" + section, option = self.__get_section_option(option) + return self._cfg[section][option].getter(section, option) + + def set(self, option, value): + """Set the value of an option. + + Throws a ``ValueError`` if `value` couldn't be converted to + ``LazyConfigOption.cls``""" + section, option = self.__get_section_option(option) + val = self._cfg[section][option].cls(value) + if not self._cfg[section][option].validate is None: + val = self._cfg[section][option].validate(val) + if not RawConfigParser.has_section(self, section): + self.add_section(section) + RawConfigParser.set(self, section, option, val) + self._modified = True + + def has_section(self, section): + """Checks if ``section`` is a known configuration section.""" + return section.lower() in self._cfg + + def has_option(self, option): + """Checks if the option (section\ **.**\ option) is a known + configuration option.""" + try: + self.__get_section_option(option) + return True + except(BadOptionError, NoSectionError, NoOptionError): + return False + + + +class LazyConfigOption(object): + """A simple container class for configuration settings. + + ``LazyConfigOption`` instances are required by `LazyConfig` instances, + and instances of classes derived from ``LazyConfig``, like the + `Config` class. + """ + __slots__ = ('cls', 'default', 'getter', 'validate') + + def __init__(self, cls, default, getter, validate=None): + """Creates a new ``LazyConfigOption`` instance. + + Arguments: + + ``cls`` : type + The class/type of the option's value + ``default`` + Default value of the option. Use ``None`` if the option should + not have a default value. + ``getter`` : callable + A method's name of `RawConfigParser` and derived classes, to + get a option's value, e.g. `self.getint`. + ``validate`` : NoneType or a callable + None or any method, that takes one argument, in order to check + the value, when `LazyConfig.set()` is called. + """ + self.cls = cls + """The class of the option's value e.g. `str`, `unicode` or `bool`""" + self.default = default + """The option's default value, may be ``None``""" + if callable(getter): + self.getter = getter + """The getter method to get the option's value""" + else: + raise TypeError('getter has to be a callable, got a %r'\ + % getter.__class__.__name__) + if validate is None or callable(validate): + self.validate = validate + """A method to validate the value""" + else: + raise TypeError('validate has to be callable or None, got a %r'\ + % validate.__class__.__name__) + + +class Config(LazyConfig): """This class is for reading and modifying vmm's configuration file.""" def __init__(self, filename): """Creates a new Config instance Arguments: - filename -- path to the configuration file + + ``filename`` + path to the configuration file """ - ConfigParser.__init__(self) + LazyConfig.__init__(self) self.__cfgFileName = filename self.__cfgFile = None - self.__VMMsections = ('account', 'bin', 'database', 'domain', - 'maildir', 'misc', 'config') - self.__changes = False self.__missing = {} - self.__dbopts = [ - ['host', 'localhot'], - ['user', 'vmm'], - ['pass', 'your secret password'], - ['name', 'mailsys'] - ] - self.__mdopts = [ - ['name', 'Maildir'], - ['folders', 'Drafts:Sent:Templates:Trash'], - ] - self.__accountopts = [ - ['delete_directory', 'false'], - ['directory_mode', 448], - ['disk_usage', 'false'], - ['password_length', 8], - ['random_password', 'false'], - ['smtp', 'true'], - ['pop3', 'true'], - ['imap', 'true'], - ['sieve', 'true'] - ] - self.__domdopts = [ - ['auto_postmaster', 'true'], - ['delete_directory', 'false'], - ['directory_mode', 504], - ['force_deletion', 'false'], - ] - self.__binopts = [ - ['dovecotpw', '/usr/sbin/dovecotpw'], - ['du', '/usr/bin/du'], - ['postconf', '/usr/sbin/postconf'] - ] - self.__miscopts = [ - ['base_directory', '/srv/mail'], - ['dovecot_version', '11'], - ['gid_mail', 8], - ['password_scheme', 'PLAIN'], - ['transport', 'dovecot:'], - ] + + LCO = LazyConfigOption + bool_t = self.bool_new + self._cfg = { + 'account': { + 'delete_directory': LCO(bool_t, False, self.get_boolean), + 'directory_mode': LCO(int, 448, self.getint), + 'disk_usage': LCO(bool_t, False, self.get_boolean), + 'password_length': LCO(int, 8, self.getint), + 'random_password': LCO(bool_t, False, self.get_boolean), + 'imap' : LCO(bool_t, True, self.get_boolean), + 'pop3' : LCO(bool_t, True, self.get_boolean), + 'sieve': LCO(bool_t, True, self.get_boolean), + 'smtp' : LCO(bool_t, True, self.get_boolean), + }, + 'bin': { + 'dovecotpw': LCO(str, '/usr/sbin/dovecotpw', self.get, + self.exec_ok), + 'du': LCO(str, '/usr/bin/du', self.get, self.exec_ok), + 'postconf': LCO(str, '/usr/sbin/postconf', self.get, + self.exec_ok), + }, + 'database': { + 'host': LCO(str, 'localhost', self.get), + 'name': LCO(str, 'mailsys', self.get), + 'pass': LCO(str, None, self.get), + 'user': LCO(str, None, self.get), + }, + 'domain': { + 'auto_postmaster': LCO(bool_t, True, self.get_boolean), + 'delete_directory': LCO(bool_t, False, self.get_boolean), + 'directory_mode': LCO(int, 504, self.getint), + 'force_deletion': LCO(bool_t, False, self.get_boolean), + }, + 'maildir': { + 'folders': LCO(str, 'Drafts:Sent:Templates:Trash', self.get), + 'name': LCO(str, 'Maildir', self.get), + }, + 'misc': { + 'base_directory': LCO(str, '/srv/mail', self.get, self.is_dir), + 'dovecot_version': LCO(int, 12, self.getint), + 'gid_mail': LCO(int, 8, self.getint), + 'password_scheme': LCO(str, 'CRAM-MD5', self.get, + self.known_scheme), + 'transport': LCO(str, 'dovecot:', self.get), + }, + 'config': {'done': LCO(bool_t, False, self.get_boolean)} + } def load(self): """Loads the configuration, read only. @@ -76,7 +329,7 @@ Raises a VMMConfigException if the configuration syntax is invalid. """ try: - self.__cfgFile = file(self.__cfgFileName, 'r') + self.__cfgFile = open(self.__cfgFileName, 'r') self.readfp(self.__cfgFile) except (MissingSectionHeaderError, ParsingError), e: self.__cfgFile.close() @@ -88,26 +341,62 @@ Raises a VMMConfigException if the check fails. """ - if not self.__chkSections(): + if not self.__chkCfg(): errmsg = StringIO() - errmsg.write(_("Using configuration file: %s\n") %\ - self.__cfgFileName) - for k,v in self.__missing.items(): - if v[0] is True: - errmsg.write(_(u"missing section: %s\n") % k) - else: - errmsg.write(_(u"missing options in section %s:\n") % k) - for o in v: - errmsg.write(" * %s\n" % o) + errmsg.write(_(u'Missing options, which have no default value.\n')) + errmsg.write(_(u'Using configuration file: %s\n') %\ + self.__cfgFileName) + for section, options in self.__missing.iteritems(): + errmsg.write(_(u'* Section: %s\n') % section) + for option in options: + errmsg.write((u' %s\n') % option) raise VMMConfigException(errmsg.getvalue(), ERR.CONF_ERROR) def getsections(self): - """Return a list with all configurable sections.""" - return self.__VMMsections[:-1] + """Returns a generator object for all configurable sections.""" + return (s for s in self._cfg.iterkeys() if s != 'config') + + def is_dir(self, path): + """Checks if ``path`` is a directory. + + Throws a `ConfigValueError` if ``path`` is not a directory. + """ + path = self.__expand_path(path) + if not os.path.isdir(path): + raise ConfigValueError(_(u'“%s” is not a directory') % \ + get_unicode(path)) + return path - def get(self, section, option, raw=False, vars=None): - return unicode(ConfigParser.get(self, section, option, raw, vars), - ENCODING, 'replace') + def exec_ok(self, binary): + """Checks if the ``binary`` exists and if it is executable. + + Throws a `ConfigValueError` if the ``binary`` isn't a file or is + not executable. + """ + binary = self.__expand_path(binary) + if not os.path.isfile(binary): + raise ConfigValueError(_(u'“%s” is not a file') % \ + get_unicode(binary)) + if not os.access(binary, os.X_OK): + raise ConfigValueError(_(u'File is not executable: “%s”') % \ + get_unicode(binary)) + return binary + + def known_scheme(self, scheme): + """Converts ``scheme`` to upper case and checks if is known by + Dovecot (listed in VirtualMailManager.SCHEMES). + + Throws a `ConfigValueError` if the scheme is not listed in + VirtualMailManager.SCHEMES. + """ + scheme = scheme.upper() + # TODO: VMM.SCHEMES + + def unicode(self, section, option): + """Returns the value of the ``option`` from ``section``, converted + to Unicode. + """ + return get_unicode(self.get(section, option)) def configure(self, sections): """Interactive method for configuring all options in the given sections @@ -115,76 +404,67 @@ Arguments: sections -- list of strings with section names """ - if not isinstance(sections, list): - raise TypeError("Argument 'sections' is not a list.") - # if [config] done = false (default at 1st run), + input_fmt = _(u'Enter new value for option %(option)s \ +[%(current_value)s]: ') + failures = 0 + + # if config.done == false (default at 1st run), # then set changes true - try: - if not self.getboolean('config', 'done'): - self.__changes = True - except ValueError: - self.set('config', 'done', 'False') - self.__changes = True + if not self.dget('config.done'): + self._modified = True w_std(_(u'Using configuration file: %s\n') % self.__cfgFileName) for s in sections: - if s != 'config': - w_std(_(u'* Config section: “%s”') % s ) + w_std(_(u'* Config section: “%s”') % s ) for opt, val in self.items(s): - newval = raw_input( - _('Enter new value for option %(opt)s [%(val)s]: ').encode( - ENCODING, 'replace') % {'opt': opt, 'val': val}) - if newval and newval != val: - self.set(s, opt, newval) - self.__changes = True + failures = 0 + while True: + newval = raw_input(input_fmt.encode(ENCODING,'replace') %{ + 'option': opt, 'current_value': val}) + if newval and newval != val: + try: + self.set('%s.%s' % (s, opt), newval) + break + except (ValueError, ConfigValueError), e: + w_std(_(u'Warning: %s') % e) + failures += 1 + if failures > 2: + raise VMMConfigException( + _(u'Too many failures - try again later.'), + ERR.VMM_TOO_MANY_FAILURES) + else: + break print - if self.__changes: + if self._modified: self.__saveChanges() def __saveChanges(self): """Writes changes to the configuration file.""" - self.set('config', 'done', 'true') + self.set('config.done', True) copy2(self.__cfgFileName, self.__cfgFileName+'.bak') - self.__cfgFile = file(self.__cfgFileName, 'w') + self.__cfgFile = open(self.__cfgFileName, 'w') self.write(self.__cfgFile) self.__cfgFile.close() - def __chkSections(self): - """Checks if all configuration sections are existing.""" + def __chkCfg(self): + """Checks all section's options for settings w/o default values. + + Returns ``True`` if everything is fine, else ``False``.""" errors = False - for s in self.__VMMsections: - if not self.has_section(s): - self.__missing[s] = [True] - errors = True - elif not self.__chkOptions(s): - errors = True + for section in self._cfg.iterkeys(): + missing = [] + for option, value in self._cfg[section].iteritems(): + if (value.default is None + and not RawConfigParser.has_option(self, section, option)): + missing.append(option) + errors = True + if len(missing): + self.__missing[section] = missing return not errors - def __chkOptions(self, section): - """Checks if all configuration options in section are existing. - - Arguments: - section -- the section to be checked - """ - retval = True - missing = [] - if section == 'database': - opts = self.__dbopts - elif section == 'maildir': - opts = self.__mdopts - elif section == 'account': - opts = self.__accountopts - elif section == 'domain': - opts = self.__domdopts - elif section == 'bin': - opts = self.__binopts - elif section == 'misc': - opts = self.__miscopts - elif section == 'config': - opts = [['done', 'false']] - for o, v in opts: - if not self.has_option(section, o): - missing.append(o) - retval = False - if len(missing): - self.__missing[section] = missing - return retval + def __expand_path(self, path): + """Expands paths, starting with ``.`` or ``~``, to an absolute path.""" + if path.startswith('.'): + return os.path.abspath(path) + if path.startswith('~'): + return os.path.expanduser(path) + return path