|
1 # -*- coding: UTF-8 -*- |
|
2 # Copyright (c) 2007 - 2012, Pascal Volk |
|
3 # See COPYING for distribution information. |
|
4 """ |
|
5 VirtualMailManager.config |
|
6 ~~~~~~~~~~~~~~~~~~~~~~~~~ |
|
7 |
|
8 VMM's configuration module for simplified configuration access. |
|
9 """ |
|
10 |
|
11 from ConfigParser import \ |
|
12 Error, MissingSectionHeaderError, NoOptionError, NoSectionError, \ |
|
13 ParsingError, RawConfigParser |
|
14 from cStringIO import StringIO |
|
15 |
|
16 from VirtualMailManager.common import VERSION_RE, \ |
|
17 exec_ok, expand_path, get_unicode, lisdir, size_in_bytes, version_hex |
|
18 from VirtualMailManager.constants import CONF_ERROR |
|
19 from VirtualMailManager.errors import ConfigError, VMMError |
|
20 from VirtualMailManager.maillocation import known_format |
|
21 from VirtualMailManager.password import verify_scheme as _verify_scheme |
|
22 |
|
23 DB_MODULES = ('psycopg2', 'pypgsql') |
|
24 DB_SSL_MODES = ('allow', 'disabled', 'prefer', 'require', 'verify-ca', |
|
25 'verify-full') |
|
26 |
|
27 _ = lambda msg: msg |
|
28 |
|
29 |
|
30 class BadOptionError(Error): |
|
31 """Raised when a option isn't in the format 'section.option'.""" |
|
32 pass |
|
33 |
|
34 |
|
35 class ConfigValueError(Error): |
|
36 """Raised when creating or validating of new values fails.""" |
|
37 pass |
|
38 |
|
39 |
|
40 class NoDefaultError(Error): |
|
41 """Raised when the requested option has no default value.""" |
|
42 |
|
43 def __init__(self, section, option): |
|
44 Error.__init__(self, 'Option %r in section %r has no default value' % |
|
45 (option, section)) |
|
46 |
|
47 |
|
48 class LazyConfig(RawConfigParser): |
|
49 """The **lazy** derivate of the `RawConfigParser`. |
|
50 |
|
51 There are two additional getters: |
|
52 |
|
53 `pget()` |
|
54 The polymorphic getter, which returns a option's value with the |
|
55 appropriate type. |
|
56 `dget()` |
|
57 Like `LazyConfig.pget()`, but returns the option's default, from |
|
58 `LazyConfig._cfg['sectionname']['optionname'].default`, if the |
|
59 option is not configured in a ini-like configuration file. |
|
60 |
|
61 `set()` differs from `RawConfigParser`'s `set()` method. `set()` |
|
62 takes the `section` and `option` arguments combined to a single |
|
63 string in the form "section.option". |
|
64 """ |
|
65 |
|
66 def __init__(self): |
|
67 RawConfigParser.__init__(self) |
|
68 self._modified = False |
|
69 # sample _cfg dict. Create your own in your derived class. |
|
70 self._cfg = { |
|
71 'sectionname': { |
|
72 'optionname': LazyConfigOption(int, 1, self.getint), |
|
73 } |
|
74 } |
|
75 |
|
76 def bool_new(self, value): |
|
77 """Converts the string `value` into a `bool` and returns it. |
|
78 |
|
79 | '1', 'on', 'yes' and 'true' will become `True` |
|
80 | '0', 'off', 'no' and 'false' will become `False` |
|
81 |
|
82 Throws a `ConfigValueError` for all other values, except bools. |
|
83 """ |
|
84 if isinstance(value, bool): |
|
85 return value |
|
86 if value.lower() in self._boolean_states: |
|
87 return self._boolean_states[value.lower()] |
|
88 else: |
|
89 raise ConfigValueError(_(u"Not a boolean: '%s'") % |
|
90 get_unicode(value)) |
|
91 |
|
92 def getboolean(self, section, option): |
|
93 """Returns the boolean value of the option, in the given |
|
94 section. |
|
95 |
|
96 For a boolean True, the value must be set to '1', 'on', 'yes', |
|
97 'true' or True. For a boolean False, the value must set to '0', |
|
98 'off', 'no', 'false' or False. |
|
99 If the option has another value assigned this method will raise |
|
100 a ValueError. |
|
101 """ |
|
102 # if the setting was modified it may be still a boolean value lets see |
|
103 tmp = self.get(section, option) |
|
104 if isinstance(tmp, bool): |
|
105 return tmp |
|
106 if not tmp.lower() in self._boolean_states: |
|
107 raise ValueError('Not a boolean: %s' % tmp) |
|
108 return self._boolean_states[tmp.lower()] |
|
109 |
|
110 def _get_section_option(self, section_option): |
|
111 """splits ``section_option`` (section.option) in two parts and |
|
112 returns them as list ``[section, option]``, if: |
|
113 |
|
114 * it likes the format of ``section_option`` |
|
115 * the ``section`` is known |
|
116 * the ``option`` is known |
|
117 |
|
118 Else one of the following exceptions will be thrown: |
|
119 |
|
120 * `BadOptionError` |
|
121 * `NoSectionError` |
|
122 * `NoOptionError` |
|
123 """ |
|
124 sect_opt = section_option.lower().split('.') |
|
125 # TODO: cache it |
|
126 if len(sect_opt) != 2 or not sect_opt[0] or not sect_opt[1]: |
|
127 raise BadOptionError(_(u"Bad format: '%s' - expected: " |
|
128 u"section.option") % |
|
129 get_unicode(section_option)) |
|
130 if not sect_opt[0] in self._cfg: |
|
131 raise NoSectionError(sect_opt[0]) |
|
132 if not sect_opt[1] in self._cfg[sect_opt[0]]: |
|
133 raise NoOptionError(sect_opt[1], sect_opt[0]) |
|
134 return sect_opt |
|
135 |
|
136 def items(self, section): |
|
137 """returns an iterable that returns key, value ``tuples`` from |
|
138 the given ``section``. |
|
139 """ |
|
140 if section in self._sections: # check if the section was parsed |
|
141 sect = self._sections[section] |
|
142 elif not section in self._cfg: |
|
143 raise NoSectionError(section) |
|
144 else: |
|
145 return ((k, self._cfg[section][k].default) \ |
|
146 for k in self._cfg[section].iterkeys()) |
|
147 # still here? Get defaults and merge defaults with configured setting |
|
148 defaults = dict((k, self._cfg[section][k].default) \ |
|
149 for k in self._cfg[section].iterkeys()) |
|
150 defaults.update(sect) |
|
151 if '__name__' in defaults: |
|
152 del defaults['__name__'] |
|
153 return defaults.iteritems() |
|
154 |
|
155 def dget(self, option): |
|
156 """Returns the value of the `option`. |
|
157 |
|
158 If the option could not be found in the configuration file, the |
|
159 configured default value, from ``LazyConfig._cfg`` will be |
|
160 returned. |
|
161 |
|
162 Arguments: |
|
163 |
|
164 `option` : string |
|
165 the configuration option in the form "section.option" |
|
166 |
|
167 Throws a `NoDefaultError`, if no default value was passed to |
|
168 `LazyConfigOption.__init__()` for the `option`. |
|
169 """ |
|
170 section, option = self._get_section_option(option) |
|
171 try: |
|
172 return self._cfg[section][option].getter(section, option) |
|
173 except (NoSectionError, NoOptionError): |
|
174 if not self._cfg[section][option].default is None: # may be False |
|
175 return self._cfg[section][option].default |
|
176 else: |
|
177 raise NoDefaultError(section, option) |
|
178 |
|
179 def pget(self, option): |
|
180 """Returns the value of the `option`.""" |
|
181 section, option = self._get_section_option(option) |
|
182 return self._cfg[section][option].getter(section, option) |
|
183 |
|
184 def set(self, option, value): |
|
185 """Set the `value` of the `option`. |
|
186 |
|
187 Throws a `ValueError` if `value` couldn't be converted using |
|
188 `LazyConfigOption.cls`. |
|
189 """ |
|
190 # pylint: disable=W0221 |
|
191 # @pylint: _L A Z Y_ |
|
192 section, option = self._get_section_option(option) |
|
193 val = self._cfg[section][option].cls(value) |
|
194 if self._cfg[section][option].validate: |
|
195 val = self._cfg[section][option].validate(val) |
|
196 if not RawConfigParser.has_section(self, section): |
|
197 self.add_section(section) |
|
198 RawConfigParser.set(self, section, option, val) |
|
199 self._modified = True |
|
200 |
|
201 def has_section(self, section): |
|
202 """Checks if `section` is a known configuration section.""" |
|
203 return section.lower() in self._cfg |
|
204 |
|
205 def has_option(self, option): |
|
206 """Checks if the option (section.option) is a known |
|
207 configuration option. |
|
208 """ |
|
209 # pylint: disable=W0221 |
|
210 # @pylint: _L A Z Y_ |
|
211 try: |
|
212 self._get_section_option(option) |
|
213 return True |
|
214 except(BadOptionError, NoSectionError, NoOptionError): |
|
215 return False |
|
216 |
|
217 def sections(self): |
|
218 """Returns an iterator object for all configuration sections.""" |
|
219 return self._cfg.iterkeys() |
|
220 |
|
221 |
|
222 class LazyConfigOption(object): |
|
223 """A simple container class for configuration settings. |
|
224 |
|
225 `LazyConfigOption` instances are required by `LazyConfig` instances, |
|
226 and instances of classes derived from `LazyConfig`, like the |
|
227 `Config` class. |
|
228 """ |
|
229 __slots__ = ('__cls', '__default', '__getter', '__validate') |
|
230 |
|
231 def __init__(self, cls, default, getter, validate=None): |
|
232 """Creates a new `LazyConfigOption` instance. |
|
233 |
|
234 Arguments: |
|
235 |
|
236 `cls` : type |
|
237 The class/type of the option's value |
|
238 `default` |
|
239 Default value of the option. Use ``None`` if the option should |
|
240 not have a default value. |
|
241 `getter` : callable |
|
242 A method's name of `RawConfigParser` and derived classes, to |
|
243 get a option's value, e.g. `self.getint`. |
|
244 `validate` : NoneType or a callable |
|
245 None or any method, that takes one argument, in order to |
|
246 check the value, when `LazyConfig.set()` is called. |
|
247 """ |
|
248 self.__cls = cls |
|
249 if not default is None: # enforce the type of the default value |
|
250 self.__default = self.__cls(default) |
|
251 else: |
|
252 self.__default = default |
|
253 if not callable(getter): |
|
254 raise TypeError('getter has to be a callable, got a %r' % |
|
255 getter.__class__.__name__) |
|
256 self.__getter = getter |
|
257 if validate and not callable(validate): |
|
258 raise TypeError('validate has to be callable or None, got a %r' % |
|
259 validate.__class__.__name__) |
|
260 self.__validate = validate |
|
261 |
|
262 @property |
|
263 def cls(self): |
|
264 """The class of the option's value e.g. `str`, `unicode` or `bool`.""" |
|
265 return self.__cls |
|
266 |
|
267 @property |
|
268 def default(self): |
|
269 """The option's default value, may be `None`""" |
|
270 return self.__default |
|
271 |
|
272 @property |
|
273 def getter(self): |
|
274 """The getter method or function to get the option's value""" |
|
275 return self.__getter |
|
276 |
|
277 @property |
|
278 def validate(self): |
|
279 """A method or function to validate the value""" |
|
280 return self.__validate |
|
281 |
|
282 |
|
283 class Config(LazyConfig): |
|
284 """This class is for reading vmm's configuration file.""" |
|
285 |
|
286 def __init__(self, filename): |
|
287 """Creates a new Config instance |
|
288 |
|
289 Arguments: |
|
290 |
|
291 `filename` : str |
|
292 path to the configuration file |
|
293 """ |
|
294 LazyConfig.__init__(self) |
|
295 self._cfg_filename = filename |
|
296 self._cfg_file = None |
|
297 self._missing = {} |
|
298 |
|
299 LCO = LazyConfigOption |
|
300 bool_t = self.bool_new |
|
301 self._cfg = { |
|
302 'account': { |
|
303 'delete_directory': LCO(bool_t, False, self.getboolean), |
|
304 'directory_mode': LCO(int, 448, self.getint), |
|
305 'disk_usage': LCO(bool_t, False, self.getboolean), |
|
306 'password_length': LCO(int, 8, self.getint), |
|
307 'random_password': LCO(bool_t, False, self.getboolean), |
|
308 }, |
|
309 'bin': { |
|
310 'dovecotpw': LCO(str, '/usr/sbin/dovecotpw', self.get, |
|
311 exec_ok), |
|
312 'du': LCO(str, '/usr/bin/du', self.get, exec_ok), |
|
313 'postconf': LCO(str, '/usr/sbin/postconf', self.get, exec_ok), |
|
314 }, |
|
315 'database': { |
|
316 'host': LCO(str, 'localhost', self.get), |
|
317 'module': LCO(str, 'psycopg2', self.get, check_db_module), |
|
318 'name': LCO(str, 'mailsys', self.get), |
|
319 'pass': LCO(str, None, self.get), |
|
320 'port': LCO(int, 5432, self.getint), |
|
321 'sslmode': LCO(str, 'prefer', self.get, check_db_ssl_mode), |
|
322 'user': LCO(str, None, self.get), |
|
323 }, |
|
324 'domain': { |
|
325 'auto_postmaster': LCO(bool_t, True, self.getboolean), |
|
326 'delete_directory': LCO(bool_t, False, self.getboolean), |
|
327 'directory_mode': LCO(int, 504, self.getint), |
|
328 'force_deletion': LCO(bool_t, False, self.getboolean), |
|
329 'imap': LCO(bool_t, True, self.getboolean), |
|
330 'pop3': LCO(bool_t, True, self.getboolean), |
|
331 'sieve': LCO(bool_t, True, self.getboolean), |
|
332 'smtp': LCO(bool_t, True, self.getboolean), |
|
333 'quota_bytes': LCO(str, '0', self.get_in_bytes, |
|
334 check_size_value), |
|
335 'quota_messages': LCO(int, 0, self.getint), |
|
336 'transport': LCO(str, 'dovecot:', self.get), |
|
337 }, |
|
338 'mailbox': { |
|
339 'folders': LCO(str, 'Drafts:Sent:Templates:Trash', |
|
340 self.unicode), |
|
341 'format': LCO(str, 'maildir', self.get, check_mailbox_format), |
|
342 'root': LCO(str, 'Maildir', self.unicode), |
|
343 'subscribe': LCO(bool_t, True, self.getboolean), |
|
344 }, |
|
345 'misc': { |
|
346 'base_directory': LCO(str, '/srv/mail', self.get, is_dir), |
|
347 'crypt_blowfish_rounds': LCO(int, 5, self.getint), |
|
348 'crypt_sha256_rounds': LCO(int, 5000, self.getint), |
|
349 'crypt_sha512_rounds': LCO(int, 5000, self.getint), |
|
350 'dovecot_version': LCO(str, None, self.hexversion, |
|
351 check_version_format), |
|
352 'password_scheme': LCO(str, 'CRAM-MD5', self.get, |
|
353 verify_scheme), |
|
354 }, |
|
355 } |
|
356 |
|
357 def load(self): |
|
358 """Loads the configuration, read only. |
|
359 |
|
360 Raises a ConfigError if the configuration syntax is |
|
361 invalid. |
|
362 """ |
|
363 self._cfg_file = open(self._cfg_filename, 'r') |
|
364 try: |
|
365 self.readfp(self._cfg_file) |
|
366 except (MissingSectionHeaderError, ParsingError), err: |
|
367 raise ConfigError(str(err), CONF_ERROR) |
|
368 self._cfg_file.close() |
|
369 |
|
370 def check(self): |
|
371 """Performs a configuration check. |
|
372 |
|
373 Raises a ConfigError if settings w/o a default value are missed. |
|
374 Or some settings have a invalid value. |
|
375 """ |
|
376 def iter_dict(): |
|
377 for section, options in self._missing.iteritems(): |
|
378 errmsg.write(_(u'* Section: %s\n') % section) |
|
379 errmsg.writelines(u' %s\n' % option for option in options) |
|
380 self._missing.clear() |
|
381 |
|
382 errmsg = None |
|
383 self._chk_non_default() |
|
384 miss_vers = 'misc' in self._missing and \ |
|
385 'dovecot_version' in self._missing['misc'] |
|
386 if self._missing: |
|
387 errmsg = StringIO() |
|
388 errmsg.write(_(u'Check of configuration file %s failed.\n') % |
|
389 self._cfg_filename) |
|
390 errmsg.write(_(u'Missing options, which have no default value.\n')) |
|
391 iter_dict() |
|
392 self._chk_possible_values(miss_vers) |
|
393 if self._missing: |
|
394 if not errmsg: |
|
395 errmsg = StringIO() |
|
396 errmsg.write(_(u'Check of configuration file %s failed.\n') % |
|
397 self._cfg_filename) |
|
398 errmsg.write(_(u'Invalid configuration values.\n')) |
|
399 else: |
|
400 errmsg.write('\n' + _(u'Invalid configuration values.\n')) |
|
401 iter_dict() |
|
402 if errmsg: |
|
403 raise ConfigError(errmsg.getvalue(), CONF_ERROR) |
|
404 |
|
405 def hexversion(self, section, option): |
|
406 """Converts the version number (e.g.: 1.2.3) from the *option*'s |
|
407 value to an int.""" |
|
408 return version_hex(self.get(section, option)) |
|
409 |
|
410 def get_in_bytes(self, section, option): |
|
411 """Converts the size value (e.g.: 1024k) from the *option*'s |
|
412 value to a long""" |
|
413 return size_in_bytes(self.get(section, option)) |
|
414 |
|
415 def unicode(self, section, option): |
|
416 """Returns the value of the `option` from `section`, converted |
|
417 to Unicode.""" |
|
418 return get_unicode(self.get(section, option)) |
|
419 |
|
420 def _chk_non_default(self): |
|
421 """Checks all section's options for settings w/o a default |
|
422 value. Missing items will be stored in _missing. |
|
423 """ |
|
424 for section in self._cfg.iterkeys(): |
|
425 missing = [] |
|
426 for option, value in self._cfg[section].iteritems(): |
|
427 if (value.default is None and |
|
428 not RawConfigParser.has_option(self, section, option)): |
|
429 missing.append(option) |
|
430 if missing: |
|
431 self._missing[section] = missing |
|
432 |
|
433 def _chk_possible_values(self, miss_vers): |
|
434 """Check settings for which the possible values are known.""" |
|
435 if not miss_vers: |
|
436 value = self.get('misc', 'dovecot_version') |
|
437 if not VERSION_RE.match(value): |
|
438 self._missing['misc'] = ['version: ' +\ |
|
439 _(u"Not a valid Dovecot version: '%s'") % value] |
|
440 # section database |
|
441 db_err = [] |
|
442 value = self.dget('database.module').lower() |
|
443 if value not in DB_MODULES: |
|
444 db_err.append('module: ' + \ |
|
445 _(u"Unsupported database module: '%s'") % value) |
|
446 if value == 'psycopg2': |
|
447 value = self.dget('database.sslmode') |
|
448 if value not in DB_SSL_MODES: |
|
449 db_err.append('sslmode: ' + \ |
|
450 _(u"Unknown pgsql SSL mode: '%s'") % value) |
|
451 if db_err: |
|
452 self._missing['database'] = db_err |
|
453 # section mailbox |
|
454 value = self.dget('mailbox.format') |
|
455 if not known_format(value): |
|
456 self._missing['mailbox'] = ['format: ' +\ |
|
457 _(u"Unsupported mailbox format: '%s'") % value] |
|
458 # section domain |
|
459 try: |
|
460 value = self.dget('domain.quota_bytes') |
|
461 except (ValueError, TypeError), err: |
|
462 self._missing['domain'] = [u'quota_bytes: ' + str(err)] |
|
463 |
|
464 |
|
465 def is_dir(path): |
|
466 """Check if the expanded path is a directory. When the expanded path |
|
467 is a directory the expanded path will be returned. Otherwise a |
|
468 ConfigValueError will be raised. |
|
469 """ |
|
470 path = expand_path(path) |
|
471 if lisdir(path): |
|
472 return path |
|
473 raise ConfigValueError(_(u"No such directory: %s") % get_unicode(path)) |
|
474 |
|
475 |
|
476 def check_db_module(module): |
|
477 """Check if the *module* is a supported pgsql module.""" |
|
478 if module.lower() in DB_MODULES: |
|
479 return module |
|
480 raise ConfigValueError(_(u"Unsupported database module: '%s'") % |
|
481 get_unicode(module)) |
|
482 |
|
483 |
|
484 def check_db_ssl_mode(ssl_mode): |
|
485 """Check if the *ssl_mode* is one of the SSL modes, known by pgsql.""" |
|
486 if ssl_mode in DB_SSL_MODES: |
|
487 return ssl_mode |
|
488 raise ConfigValueError(_(u"Unknown pgsql SSL mode: '%s'") % |
|
489 get_unicode(ssl_mode)) |
|
490 |
|
491 |
|
492 def check_mailbox_format(format): |
|
493 """ |
|
494 Check if the mailbox format *format* is supported. When the *format* |
|
495 is supported it will be returned, otherwise a `ConfigValueError` will |
|
496 be raised. |
|
497 """ |
|
498 format = format.lower() |
|
499 if known_format(format): |
|
500 return format |
|
501 raise ConfigValueError(_(u"Unsupported mailbox format: '%s'") % |
|
502 get_unicode(format)) |
|
503 |
|
504 |
|
505 def check_size_value(value): |
|
506 """Check if the size value *value* has the proper format, e.g.: 1024k. |
|
507 Returns the validated value string if it has the expected format. |
|
508 Otherwise a `ConfigValueError` will be raised.""" |
|
509 try: |
|
510 tmp = size_in_bytes(value) |
|
511 except (TypeError, ValueError), err: |
|
512 raise ConfigValueError(_(u"Not a valid size value: '%s'") % |
|
513 get_unicode(value)) |
|
514 return value |
|
515 |
|
516 |
|
517 def check_version_format(version_string): |
|
518 """Check if the *version_string* has the proper format, e.g.: '1.2.3'. |
|
519 Returns the validated version string if it has the expected format. |
|
520 Otherwise a `ConfigValueError` will be raised. |
|
521 """ |
|
522 if not VERSION_RE.match(version_string): |
|
523 raise ConfigValueError(_(u"Not a valid Dovecot version: '%s'") % |
|
524 get_unicode(version_string)) |
|
525 return version_string |
|
526 |
|
527 |
|
528 def verify_scheme(scheme): |
|
529 """Checks if the password scheme *scheme* can be accepted and returns |
|
530 the verified scheme. |
|
531 """ |
|
532 try: |
|
533 scheme, encoding = _verify_scheme(scheme) |
|
534 except VMMError, err: # 'cast' it |
|
535 raise ConfigValueError(err.msg) |
|
536 if not encoding: |
|
537 return scheme |
|
538 return '%s.%s' % (scheme, encoding) |
|
539 |
|
540 del _ |