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 |
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 |