f5b171c6783107b16e4e8cb4404c07bafb003fec
[tiramisu.git] / tiramisu / option.py
1 # -*- coding: utf-8 -*-
2 "option types and option description"
3 # Copyright (C) 2012-2013 Team tiramisu (see AUTHORS for all contributors)
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18 #
19 # The original `Config` design model is unproudly borrowed from
20 # the rough pypy's guys: http://codespeak.net/svn/pypy/dist/pypy/config/
21 # the whole pypy projet is under MIT licence
22 # ____________________________________________________________
23 import re
24 import sys
25 from copy import copy, deepcopy
26 from types import FunctionType
27 from IPy import IP
28
29 from tiramisu.error import ConflictError
30 from tiramisu.setting import groups, multitypes
31 from tiramisu.i18n import _
32 from tiramisu.autolib import carry_out_calculation
33
34 name_regexp = re.compile(r'^\d+')
35 forbidden_names = ('iter_all', 'iter_group', 'find', 'find_first',
36                    'make_dict', 'unwrap_from_path', 'read_only',
37                    'read_write', 'getowner', 'set_contexts')
38
39
40 def valid_name(name):
41     "an option's name is a str and does not start with 'impl' or 'cfgimpl'"
42     try:
43         name = str(name)
44     except:
45         return False
46     if re.match(name_regexp, name) is None and not name.startswith('_') \
47             and name not in forbidden_names \
48             and not name.startswith('impl_') \
49             and not name.startswith('cfgimpl_'):
50         return True
51     else:
52         return False
53 #____________________________________________________________
54 #
55
56
57 class BaseInformation(object):
58     "interface for an option's information attribute"
59     __slots__ = ('_impl_informations',)
60
61     def impl_set_information(self, key, value):
62         """updates the information's attribute
63         (which is a dictionary)
64
65         :param key: information's key (ex: "help", "doc"
66         :param value: information's value (ex: "the help string")
67         """
68         try:
69             self._impl_informations[key] = value
70         except AttributeError:
71             raise AttributeError(_('{0} has no attribute '
72                                    'impl_set_information').format(
73                                        self.__class__.__name__))
74
75     def impl_get_information(self, key, default=None):
76         """retrieves one information's item
77
78         :param key: the item string (ex: "help")
79         """
80         try:
81             if key in self._impl_informations:
82                 return self._impl_informations[key]
83             elif default is not None:
84                 return default
85             else:
86                 raise ValueError(_("information's item"
87                                    " not found: {0}").format(key))
88         except AttributeError:
89             raise AttributeError(_('{0} has no attribute '
90                                    'impl_get_information').format(
91                                        self.__class__.__name__))
92
93
94 class BaseOption(BaseInformation):
95     """This abstract base class stands for attribute access
96     in options that have to be set only once, it is of course done in the
97     __setattr__ method
98     """
99     __slots__ = ('_name', '_requires', '_properties', '_readonly',
100                  '_consistencies', '_calc_properties', '_state_consistencies',
101                  '_state_requires', '_stated')
102
103     def __init__(self, name, doc, requires, properties):
104         if not valid_name(name):
105             raise ValueError(_("invalid name: {0} for option").format(name))
106         self._name = name
107         self._impl_informations = {}
108         self.impl_set_information('doc', doc)
109         self._calc_properties, self._requires = validate_requires_arg(
110             requires, self._name)
111         self._consistencies = None
112         if properties is None:
113             properties = tuple()
114         if not isinstance(properties, tuple):
115             raise TypeError(_('invalid properties type {0} for {1},'
116                             ' must be a tuple').format(
117                                 type(properties),
118                                 self._name))
119         if self._calc_properties is not None and properties is not tuple():
120             set_forbidden_properties = set(properties) & self._calc_properties
121             if set_forbidden_properties != frozenset():
122                 raise ValueError('conflict: properties already set in '
123                                  'requirement {0}'.format(
124                                      list(set_forbidden_properties)))
125         self._properties = properties  # 'hidden', 'disabled'...
126
127     def __setattr__(self, name, value):
128         """set once and only once some attributes in the option,
129         like `_name`. `_name` cannot be changed one the option and
130         pushed in the :class:`tiramisu.option.OptionDescription`.
131
132         if the attribute `_readonly` is set to `True`, the option is
133         "frozen" (which has noting to do with the high level "freeze"
134         propertie or "read_only" property)
135         """
136         if not name.startswith('_state'):
137             is_readonly = False
138             # never change _name
139             if name == '_name':
140                 try:
141                     self._name
142                     #so _name is already set
143                     is_readonly = True
144                 except:
145                     pass
146             try:
147                 if self._readonly is True:
148                     if value is True:
149                         # already readonly and try to re set readonly
150                         # don't raise, just exit
151                         return
152                     is_readonly = True
153             except AttributeError:
154                 pass
155             if is_readonly:
156                 raise AttributeError(_("'{0}' ({1}) object attribute '{2}' is"
157                                        " read-only").format(
158                                            self.__class__.__name__,
159                                            self._name,
160                                            name))
161         object.__setattr__(self, name, value)
162
163     def _impl_convert_consistencies(self, cache):
164         # cache is a dico in import/not a dico in export
165         if self._consistencies is None:
166             self._state_consistencies = None
167         else:
168             new_value = []
169             for consistency in self._consistencies:
170                 if isinstance(cache, dict):
171                     new_value.append((consistency[0], cache[consistency[1]]))
172                 else:
173                     new_value.append((consistency[0],
174                                       cache.impl_get_path_by_opt(
175                                           consistency[1])))
176             if isinstance(cache, dict):
177                 pass
178             else:
179                 self._state_consistencies = tuple(new_value)
180
181     def _impl_convert_requires(self, cache):
182         # cache is a dico in import/not a dico in export
183         if self._requires is None:
184             self._state_requires = None
185         else:
186             new_value = []
187             for requires in self._requires:
188                 new_requires = []
189                 for require in requires:
190                     if isinstance(cache, dict):
191                         new_require = [cache[require[0]]]
192                     else:
193                         new_require = [cache.impl_get_path_by_opt(require[0])]
194                     new_require.extend(require[1:])
195                     new_requires.append(tuple(new_require))
196                 new_value.append(tuple(new_requires))
197             if isinstance(cache, dict):
198                 pass
199             else:
200                 self._state_requires = new_value
201
202     def _impl_getstate(self, descr):
203         self._stated = True
204         self._impl_convert_consistencies(descr)
205         self._impl_convert_requires(descr)
206
207     def __getstate__(self, export=False):
208         try:
209             self._stated
210         except AttributeError:
211             raise SystemError(_('cannot serialize Option, only in OptionDescription'))
212         slots = set()
213         for subclass in self.__class__.__mro__:
214             if subclass is not object:
215                 slots.update(subclass.__slots__)
216         slots -= frozenset(['_cache_paths', '__weakref__'])
217         states = {}
218         for slot in slots:
219             # remove variable if save variable converted in _state_xxxx variable
220             if '_state' + slot not in slots:
221                 if slot.startswith('_state'):
222                     # should exists
223                     states[slot] = getattr(self, slot)
224                     # remove _state_xxx variable
225                     self.__delattr__(slot)
226                 else:
227                     try:
228                         states[slot] = getattr(self, slot)
229                     except AttributeError:
230                         pass
231         return states
232
233
234 class Option(BaseOption):
235     """
236     Abstract base class for configuration option's.
237
238     Reminder: an Option object is **not** a container for the value
239     """
240     __slots__ = ('_multi', '_validator', '_default_multi', '_default', '_callback',
241                  '_multitype', '_master_slaves', '__weakref__')
242     _empty = ''
243
244     def __init__(self, name, doc, default=None, default_multi=None,
245                  requires=None, multi=False, callback=None,
246                  callback_params=None, validator=None, validator_args=None,
247                  properties=None):
248         """
249         :param name: the option's name
250         :param doc: the option's description
251         :param default: specifies the default value of the option,
252                         for a multi : ['bla', 'bla', 'bla']
253         :param default_multi: 'bla' (used in case of a reset to default only at
254                         a given index)
255         :param requires: is a list of names of options located anywhere
256                          in the configuration.
257         :param multi: if true, the option's value is a list
258         :param callback: the name of a function. If set, the function's output
259                          is responsible of the option's value
260         :param callback_params: the callback's parameter
261         :param validator: the name of a function wich stands for a custom
262                           validation of the value
263         :param validator_args: the validator's parameters
264
265         """
266         super(Option, self).__init__(name, doc, requires, properties)
267         self._multi = multi
268         if validator is not None:
269             if type(validator) != FunctionType:
270                 raise TypeError(_("validator must be a function"))
271             if validator_args is None:
272                 validator_args = {}
273             self._validator = (validator, validator_args)
274         else:
275             self._validator = None
276         if not self._multi and default_multi is not None:
277             raise ValueError(_("a default_multi is set whereas multi is False"
278                              " in option: {0}").format(name))
279         if default_multi is not None:
280             try:
281                 self._validate(default_multi)
282             except ValueError as err:
283                 raise ValueError(_("invalid default_multi value {0} "
284                                    "for option {1}: {2}").format(
285                                        str(default_multi), name, err))
286         if callback is not None and (default is not None or
287                                      default_multi is not None):
288             raise ValueError(_("default value not allowed if option: {0} "
289                              "is calculated").format(name))
290         if callback is None and callback_params is not None:
291             raise ValueError(_("params defined for a callback function but "
292                              "no callback defined"
293                              " yet for option {0}").format(name))
294         if callback is not None:
295             if type(callback) != FunctionType:
296                 raise ValueError('callback must be a function')
297             if callback_params is not None and \
298                     not isinstance(callback_params, dict):
299                 raise ValueError('callback_params must be a dict')
300             self._callback = (callback, callback_params)
301         else:
302             self._callback = None
303         if self._multi:
304             if default is None:
305                 default = []
306             self._multitype = multitypes.default
307             self._default_multi = default_multi
308         self.impl_validate(default)
309         self._default = default
310
311     def _launch_consistency(self, func, opt, vals, context, index, opt_):
312         if context is not None:
313             descr = context.cfgimpl_get_description()
314         if opt is self:
315             #values are for self, search opt_ values
316             values = vals
317             if context is not None:
318                 path = descr.impl_get_path_by_opt(opt_)
319                 values_ = context._getattr(path, validate=False)
320             else:
321                 values_ = opt_.impl_getdefault()
322             if index is not None:
323                 #value is not already set, could be higher
324                 try:
325                     values_ = values_[index]
326                 except IndexError:
327                     values_ = None
328         else:
329             #values are for opt_, search self values
330             values_ = vals
331             if context is not None:
332                 path = descr.impl_get_path_by_opt(self)
333                 values = context._getattr(path, validate=False)
334             else:
335                 values = self.impl_getdefault()
336             if index is not None:
337                 #value is not already set, could be higher
338                 try:
339                     values = values[index]
340                 except IndexError:
341                     values = None
342         if index is None and self.impl_is_multi():
343             for index in range(0, len(values)):
344                 try:
345                     value = values[index]
346                     value_ = values_[index]
347                 except IndexError:
348                     value = None
349                     value_ = None
350                 if None not in (value, value_):
351                     getattr(self, func)(opt_._name, value, value_)
352         else:
353             if None not in (values, values_):
354                 getattr(self, func)(opt_._name, values, values_)
355
356     def impl_validate(self, value, context=None, validate=True):
357         """
358         :param value: the option's value
359         :param validate: if true enables ``self._validator`` validation
360         """
361         if not validate:
362             return
363
364         def val_validator(val):
365             if self._validator is not None:
366                 callback_params = deepcopy(self._validator[1])
367                 callback_params.setdefault('', []).insert(0, val)
368                 return carry_out_calculation(self._name, config=context,
369                                              callback=self._validator[0],
370                                              callback_params=callback_params)
371             else:
372                 return True
373
374         def do_validation(_value, _index=None):
375             if _value is None:
376                 return True
377             if not val_validator(_value):
378                 raise ValueError(_("invalid value {0} "
379                                    "for option {1} for object {2}"
380                                    ).format(_value,
381                                             self._name,
382                                             self.__class__.__name__))
383             try:
384                 self._validate(_value)
385             except ValueError as err:
386                 raise ValueError(_("invalid value {0} for option {1}: {2}"
387                                    "").format(_value, self._name, err))
388             if context is not None:
389                 descr._valid_consistency(self, _value, context, _index)
390
391         # generic calculation
392         if context is not None:
393             descr = context.cfgimpl_get_description()
394         if not self._multi:
395             do_validation(value)
396         else:
397             if not isinstance(value, list):
398                 raise ValueError(_("invalid value {0} for option {1} "
399                                    "which must be a list").format(value,
400                                                                   self._name))
401             for index in range(0, len(value)):
402                 val = value[index]
403                 do_validation(val, index)
404
405     def impl_getdefault(self, default_multi=False):
406         "accessing the default value"
407         if not default_multi or not self.impl_is_multi():
408             return self._default
409         else:
410             return self.getdefault_multi()
411
412     def impl_getdefault_multi(self):
413         "accessing the default value for a multi"
414         return self._default_multi
415
416     def impl_get_multitype(self):
417         return self._multitype
418
419     def impl_get_master_slaves(self):
420         return self._master_slaves
421
422     def impl_is_empty_by_default(self):
423         "no default value has been set yet"
424         if ((not self.impl_is_multi() and self._default is None) or
425                 (self.impl_is_multi() and (self._default == []
426                                            or None in self._default))):
427             return True
428         return False
429
430     def impl_getdoc(self):
431         "accesses the Option's doc"
432         return self.impl_get_information('doc')
433
434     def impl_has_callback(self):
435         "to know if a callback has been defined or not"
436         if self._callback is None:
437             return False
438         else:
439             return True
440
441     def impl_getkey(self, value):
442         return value
443
444     def impl_is_multi(self):
445         return self._multi
446
447     def impl_add_consistency(self, func, opt):
448         if self._consistencies is None:
449             self._consistencies = []
450         if not isinstance(opt, Option):
451             raise ValueError('consistency must be set with an option')
452         if self is opt:
453             raise ValueError('cannot add consistency with itself')
454         if self.impl_is_multi() != opt.impl_is_multi():
455             raise ValueError('options in consistency'
456                              ' should be multi in two sides')
457         func = '_cons_{0}'.format(func)
458         self._launch_consistency(func,
459                                  self,
460                                  self.impl_getdefault(),
461                                  None, None, opt)
462         self._consistencies.append((func, opt))
463         self.impl_validate(self.impl_getdefault())
464
465     def _cons_not_equal(self, optname, value, value_):
466         if value == value_:
467             raise ValueError(_("invalid value {0} for option {1} "
468                                "must be different as {2} option"
469                                "").format(value, self._name, optname))
470
471
472 class ChoiceOption(Option):
473     """represents a choice out of several objects.
474
475     The option can also have the value ``None``
476     """
477
478     __slots__ = ('_values', '_open_values')
479     _opt_type = 'string'
480
481     def __init__(self, name, doc, values, default=None, default_multi=None,
482                  requires=None, multi=False, callback=None,
483                  callback_params=None, open_values=False, validator=None,
484                  validator_args=None, properties=()):
485         """
486         :param values: is a list of values the option can possibly take
487         """
488         if not isinstance(values, tuple):
489             raise TypeError(_('values must be a tuple for {0}').format(name))
490         self._values = values
491         if open_values not in (True, False):
492             raise TypeError(_('open_values must be a boolean for '
493                             '{0}').format(name))
494         self._open_values = open_values
495         super(ChoiceOption, self).__init__(name, doc, default=default,
496                                            default_multi=default_multi,
497                                            callback=callback,
498                                            callback_params=callback_params,
499                                            requires=requires,
500                                            multi=multi,
501                                            validator=validator,
502                                            validator_args=validator_args,
503                                            properties=properties)
504
505     def impl_get_values(self):
506         return self._values
507
508     def impl_is_openvalues(self):
509         return self._open_values
510
511     def _validate(self, value):
512         if not self._open_values and not value in self._values:
513             raise ValueError(_('value {0} is not permitted, '
514                                'only {1} is allowed'
515                                '').format(value, self._values))
516
517
518 class BoolOption(Option):
519     "represents a choice between ``True`` and ``False``"
520     __slots__ = tuple()
521     _opt_type = 'bool'
522
523     def _validate(self, value):
524         if not isinstance(value, bool):
525             raise ValueError(_('value must be a boolean'))
526
527
528 class IntOption(Option):
529     "represents a choice of an integer"
530     __slots__ = tuple()
531     _opt_type = 'int'
532
533     def _validate(self, value):
534         if not isinstance(value, int):
535             raise ValueError(_('value must be an integer'))
536
537
538 class FloatOption(Option):
539     "represents a choice of a floating point number"
540     __slots__ = tuple()
541     _opt_type = 'float'
542
543     def _validate(self, value):
544         if not isinstance(value, float):
545             raise ValueError(_('value must be a float'))
546
547
548 class StrOption(Option):
549     "represents the choice of a string"
550     __slots__ = tuple()
551     _opt_type = 'string'
552
553     def _validate(self, value):
554         if not isinstance(value, str):
555             raise ValueError(_('value must be a string, not '
556                                '{0}').format(type(value)))
557
558
559 if sys.version_info[0] >= 3:
560     #UnicodeOption is same has StrOption in python 3+
561     class UnicodeOption(StrOption):
562         __slots__ = tuple()
563         pass
564 else:
565     class UnicodeOption(Option):
566         "represents the choice of a unicode string"
567         __slots__ = tuple()
568         _opt_type = 'unicode'
569         _empty = u''
570
571         def _validate(self, value):
572             if not isinstance(value, unicode):
573                 raise ValueError(_('value must be an unicode'))
574
575
576 class SymLinkOption(BaseOption):
577     __slots__ = ('_name', '_opt', '_state_opt')
578     _opt_type = 'symlink'
579
580     def __init__(self, name, opt):
581         self._name = name
582         if not isinstance(opt, Option):
583             raise ValueError(_('malformed symlinkoption '
584                                'must be an option '
585                                'for symlink {0}').format(name))
586         self._opt = opt
587         self._readonly = True
588
589     def __getattr__(self, name):
590         if name in ('_name', '_opt', '_opt_type', '_readonly'):
591             return object.__getattr__(self, name)
592         else:
593             return getattr(self._opt, name)
594
595     def _impl_getstate(self, descr):
596         super(SymLinkOption, self)._impl_getstate(descr)
597         self._state_opt = descr.impl_get_path_by_opt(self._opt)
598
599
600 class IPOption(Option):
601     "represents the choice of an ip"
602     __slots__ = ('_only_private',)
603     _opt_type = 'ip'
604
605     def __init__(self, name, doc, default=None, default_multi=None,
606                  requires=None, multi=False, callback=None,
607                  callback_params=None, validator=None, validator_args=None,
608                  properties=None, only_private=False):
609         self._only_private = only_private
610         super(IPOption, self).__init__(name, doc, default=default,
611                                        default_multi=default_multi,
612                                        callback=callback,
613                                        callback_params=callback_params,
614                                        requires=requires,
615                                        multi=multi,
616                                        validator=validator,
617                                        validator_args=validator_args,
618                                        properties=properties)
619
620     def _validate(self, value):
621         ip = IP('{0}/32'.format(value))
622         if ip.iptype() == 'RESERVED':
623             raise ValueError(_("IP mustn't not be in reserved class"))
624         if self._only_private and not ip.iptype() == 'PRIVATE':
625             raise ValueError(_("IP must be in private class"))
626
627
628 class PortOption(Option):
629     """represents the choice of a port
630     The port numbers are divided into three ranges:
631     the well-known ports,
632     the registered ports,
633     and the dynamic or private ports.
634     You can actived this three range.
635     Port number 0 is reserved and can't be used.
636     see: http://en.wikipedia.org/wiki/Port_numbers
637     """
638     __slots__ = ('_allow_range', '_allow_zero', '_min_value', '_max_value')
639     _opt_type = 'port'
640
641     def __init__(self, name, doc, default=None, default_multi=None,
642                  requires=None, multi=False, callback=None,
643                  callback_params=None, validator=None, validator_args=None,
644                  properties=None, allow_range=False, allow_zero=False,
645                  allow_wellknown=True, allow_registred=True,
646                  allow_private=False):
647         self._allow_range = allow_range
648         self._min_value = None
649         self._max_value = None
650         ports_min = [0, 1, 1024, 49152]
651         ports_max = [0, 1023, 49151, 65535]
652         is_finally = False
653         for index, allowed in enumerate([allow_zero,
654                                          allow_wellknown,
655                                          allow_registred,
656                                          allow_private]):
657             if self._min_value is None:
658                 if allowed:
659                     self._min_value = ports_min[index]
660             elif not allowed:
661                 is_finally = True
662             elif allowed and is_finally:
663                 raise ValueError(_('inconsistency in allowed range'))
664             if allowed:
665                 self._max_value = ports_max[index]
666
667         if self._max_value is None:
668             raise ValueError(_('max value is empty'))
669
670         super(PortOption, self).__init__(name, doc, default=default,
671                                          default_multi=default_multi,
672                                          callback=callback,
673                                          callback_params=callback_params,
674                                          requires=requires,
675                                          multi=multi,
676                                          validator=validator,
677                                          validator_args=validator_args,
678                                          properties=properties)
679
680     def _validate(self, value):
681         if self._allow_range and ":" in str(value):
682             value = str(value).split(':')
683             if len(value) != 2:
684                 raise ValueError('range must have two values only')
685             if not value[0] < value[1]:
686                 raise ValueError('first port in range must be'
687                                  ' smaller than the second one')
688         else:
689             value = [value]
690
691         for val in value:
692             if not self._min_value <= int(val) <= self._max_value:
693                 raise ValueError('port must be an between {0} and {1}'
694                                  ''.format(self._min_value, self._max_value))
695
696
697 class NetworkOption(Option):
698     "represents the choice of a network"
699     __slots__ = tuple()
700     _opt_type = 'network'
701
702     def _validate(self, value):
703         ip = IP(value)
704         if ip.iptype() == 'RESERVED':
705             raise ValueError(_("network mustn't not be in reserved class"))
706
707
708 class NetmaskOption(Option):
709     "represents the choice of a netmask"
710     __slots__ = tuple()
711     _opt_type = 'netmask'
712
713     def _validate(self, value):
714         IP('0.0.0.0/{0}'.format(value))
715
716     def _cons_network_netmask(self, optname, value, value_):
717         #opts must be (netmask, network) options
718         self.__cons_netmask(optname, value, value_, False)
719
720     def _cons_ip_netmask(self, optname, value, value_):
721         #opts must be (netmask, ip) options
722         self.__cons_netmask(optname, value, value_, True)
723
724     #def __cons_netmask(self, opt, value, context, index, opts, make_net):
725     def __cons_netmask(self, optname, val_netmask, val_ipnetwork, make_net):
726         msg = None
727         try:
728             ip = IP('{0}/{1}'.format(val_ipnetwork, val_netmask),
729                     make_net=make_net)
730             #if cidr == 32, ip same has network
731             if ip.prefixlen() != 32:
732                 try:
733                     IP('{0}/{1}'.format(val_ipnetwork, val_netmask),
734                         make_net=not make_net)
735                 except ValueError:
736                     if not make_net:
737                         msg = _("invalid network {0} ({1}) "
738                                 "with netmask {2} ({3}),"
739                                 " this network is an IP")
740                 else:
741                     if make_net:
742                         msg = _("invalid IP {0} ({1}) with netmask {2} ({3}),"
743                                 " this IP is a network")
744
745         except ValueError:
746             if make_net:
747                 msg = _("invalid IP {0} ({1}) with netmask {2} ({3})")
748             else:
749                 msg = _("invalid network {0} ({1}) with netmask {2} ({3})")
750         if msg is not None:
751             raise ValueError(msg.format(val_ipnetwork, optname,
752                                         val_netmask, self._name))
753
754
755 class DomainnameOption(Option):
756     "represents the choice of a domain name"
757     __slots__ = ('_type', '_allow_ip')
758     _opt_type = 'domainname'
759
760     def __init__(self, name, doc, default=None, default_multi=None,
761                  requires=None, multi=False, callback=None,
762                  callback_params=None, validator=None, validator_args=None,
763                  properties=None, allow_ip=False, type_='domainname'):
764         #netbios: for MS domain
765         #hostname: to identify the device
766         #domainname:
767         #fqdn: with tld, not supported yet
768         if type_ not in ['netbios', 'hostname', 'domainname']:
769             raise ValueError(_('unknown type_ {0} for hostname').format(type_))
770         self._type = type_
771         if allow_ip not in [True, False]:
772             raise ValueError(_('allow_ip must be a boolean'))
773         self._allow_ip = allow_ip
774         super(DomainnameOption, self).__init__(name, doc, default=default,
775                                                default_multi=default_multi,
776                                                callback=callback,
777                                                callback_params=callback_params,
778                                                requires=requires,
779                                                multi=multi,
780                                                validator=validator,
781                                                validator_args=validator_args,
782                                                properties=properties)
783
784     def _validate(self, value):
785         if self._allow_ip is True:
786             try:
787                 IP('{0}/32'.format(value))
788                 return
789             except ValueError:
790                 pass
791         if self._type == 'netbios':
792             length = 15
793             extrachar = ''
794         elif self._type == 'hostname':
795             length = 63
796             extrachar = ''
797         elif self._type == 'domainname':
798             length = 255
799             extrachar = '\.'
800             if '.' not in value:
801                 raise ValueError(_("invalid value for {0}, must have dot"
802                                    "").format(self._name))
803         if len(value) > length:
804             raise ValueError(_("invalid domainname's length for"
805                                " {0} (max {1})").format(self._name, length))
806         if len(value) == 1:
807             raise ValueError(_("invalid domainname's length for {0} (min 2)"
808                                "").format(self._name))
809         regexp = r'^[a-z]([a-z\d{0}-])*[a-z\d]$'.format(extrachar)
810         if re.match(regexp, value) is None:
811             raise ValueError(_('invalid domainname'))
812
813
814 class OptionDescription(BaseOption):
815     """Config's schema (organisation, group) and container of Options
816     The `OptionsDescription` objects lives in the `tiramisu.config.Config`.
817     """
818     __slots__ = ('_name', '_requires', '_cache_paths', '_group_type',
819                  '_state_group_type', '_properties', '_children',
820                  '_consistencies', '_calc_properties', '__weakref__',
821                  '_readonly', '_impl_informations', '_state_requires',
822                  '_state_consistencies', '_stated')
823     _opt_type = 'optiondescription'
824
825     def __init__(self, name, doc, children, requires=None, properties=None):
826         """
827         :param children: a list of options (including optiondescriptions)
828
829         """
830         super(OptionDescription, self).__init__(name, doc, requires, properties)
831         child_names = [child._name for child in children]
832         #better performance like this
833         valid_child = copy(child_names)
834         valid_child.sort()
835         old = None
836         for child in valid_child:
837             if child == old:
838                 raise ConflictError(_('duplicate option name: '
839                                       '{0}').format(child))
840             old = child
841         self._children = (tuple(child_names), tuple(children))
842         self._cache_paths = None
843         # the group_type is useful for filtering OptionDescriptions in a config
844         self._group_type = groups.default
845
846     def impl_getdoc(self):
847         return self.impl_get_information('doc')
848
849     def __getattr__(self, name):
850         if name in self.__slots__:
851             return object.__getattribute__(self, name)
852         try:
853             return self._children[1][self._children[0].index(name)]
854         except ValueError:
855             raise AttributeError(_('unknown Option {0} '
856                                    'in OptionDescription {1}'
857                                    '').format(name, self._name))
858
859     def impl_getkey(self, config):
860         return tuple([child.impl_getkey(getattr(config, child._name))
861                       for child in self.impl_getchildren()])
862
863     def impl_getpaths(self, include_groups=False, _currpath=None):
864         """returns a list of all paths in self, recursively
865            _currpath should not be provided (helps with recursion)
866         """
867         if _currpath is None:
868             _currpath = []
869         paths = []
870         for option in self.impl_getchildren():
871             attr = option._name
872             if isinstance(option, OptionDescription):
873                 if include_groups:
874                     paths.append('.'.join(_currpath + [attr]))
875                 paths += option.impl_getpaths(include_groups=include_groups,
876                                               _currpath=_currpath + [attr])
877             else:
878                 paths.append('.'.join(_currpath + [attr]))
879         return paths
880
881     def impl_getchildren(self):
882         return self._children[1]
883
884     def impl_build_cache(self,
885                          cache_path=None,
886                          cache_option=None,
887                          _currpath=None,
888                          _consistencies=None):
889         if _currpath is None and self._cache_paths is not None:
890             # cache already set
891             return
892         if _currpath is None:
893             save = True
894             _currpath = []
895             _consistencies = {}
896         else:
897             save = False
898         if cache_path is None:
899             cache_path = []
900             cache_option = []
901         for option in self.impl_getchildren():
902             attr = option._name
903             if option in cache_option:
904                 raise ConflictError(_('duplicate option: {0}').format(option))
905
906             cache_option.append(option)
907             option._readonly = True
908             cache_path.append(str('.'.join(_currpath + [attr])))
909             if not isinstance(option, OptionDescription):
910                 if option._consistencies is not None:
911                     for consistency in option._consistencies:
912                         func, opt = consistency
913                         opts = (option, opt)
914                         _consistencies.setdefault(opt,
915                                                   []).append((func, opts))
916                         _consistencies.setdefault(option,
917                                                   []).append((func, opts))
918             else:
919                 _currpath.append(attr)
920                 option.impl_build_cache(cache_path,
921                                         cache_option,
922                                         _currpath,
923                                         _consistencies)
924                 _currpath.pop()
925         if save:
926             self._cache_paths = (tuple(cache_option), tuple(cache_path))
927             self._consistencies = _consistencies
928             self._readonly = True
929
930     def impl_get_opt_by_path(self, path):
931         try:
932             return self._cache_paths[0][self._cache_paths[1].index(path)]
933         except ValueError:
934             raise AttributeError(_('no option for path {0}').format(path))
935
936     def impl_get_path_by_opt(self, opt):
937         try:
938             return self._cache_paths[1][self._cache_paths[0].index(opt)]
939         except ValueError:
940             raise AttributeError(_('no option {0} found').format(opt))
941
942     # ____________________________________________________________
943     def impl_set_group_type(self, group_type):
944         """sets a given group object to an OptionDescription
945
946         :param group_type: an instance of `GroupType` or `MasterGroupType`
947                               that lives in `setting.groups`
948         """
949         if self._group_type != groups.default:
950             raise TypeError(_('cannot change group_type if already set '
951                             '(old {0}, new {1})').format(self._group_type,
952                                                          group_type))
953         if isinstance(group_type, groups.GroupType):
954             self._group_type = group_type
955             if isinstance(group_type, groups.MasterGroupType):
956                 #if master (same name has group) is set
957                 identical_master_child_name = False
958                 #for collect all slaves
959                 slaves = []
960                 master = None
961                 for child in self.impl_getchildren():
962                     if isinstance(child, OptionDescription):
963                         raise ValueError(_("master group {0} shall not have "
964                                          "a subgroup").format(self._name))
965                     if isinstance(child, SymLinkOption):
966                         raise ValueError(_("master group {0} shall not have "
967                                          "a symlinkoption").format(self._name))
968                     if not child.impl_is_multi():
969                         raise ValueError(_("not allowed option {0} "
970                                          "in group {1}"
971                                          ": this option is not a multi"
972                                          "").format(child._name, self._name))
973                     if child._name == self._name:
974                         identical_master_child_name = True
975                         child._multitype = multitypes.master
976                         master = child
977                     else:
978                         slaves.append(child)
979                 if master is None:
980                     raise ValueError(_('master group with wrong'
981                                        ' master name for {0}'
982                                        ).format(self._name))
983                 master._master_slaves = tuple(slaves)
984                 for child in self.impl_getchildren():
985                     if child != master:
986                         child._master_slaves = master
987                         child._multitype = multitypes.slave
988                 if not identical_master_child_name:
989                     raise ValueError(_("no child has same nom has master group"
990                                        " for: {0}").format(self._name))
991         else:
992             raise ValueError(_('group_type: {0}'
993                                ' not allowed').format(group_type))
994
995     def impl_get_group_type(self):
996         return self._group_type
997
998     def _valid_consistency(self, opt, value, context=None, index=None):
999         consistencies = self._consistencies.get(opt)
1000         if consistencies is not None:
1001             for consistency in consistencies:
1002                 opt_ = consistency[1]
1003                 ret = opt_[0]._launch_consistency(consistency[0],
1004                                                   opt,
1005                                                   value,
1006                                                   context,
1007                                                   index,
1008                                                   opt_[1])
1009                 if ret is False:
1010                     return False
1011         return True
1012
1013     def _impl_getstate(self, descr=None):
1014         """enables us to export into a dict
1015         :param descr: parent :class:`tiramisu.option.OptionDescription`
1016         """
1017         if descr is None:
1018             self.impl_build_cache()
1019             descr = self
1020         super(OptionDescription, self)._impl_getstate(descr)
1021         self._state_group_type = str(self._group_type)
1022         for option in self.impl_getchildren():
1023             option._impl_getstate(descr)
1024
1025     def __getstate__(self):
1026         try:
1027             del(self._stated)
1028         except AttributeError:
1029             # if cannot delete, _impl_getstate never launch
1030             # launch it recursivement
1031             # _stated prevent __getstate__ launch more than one time
1032             # _stated is delete, if re-serialize, re-lauch _impl_getstate
1033             self._impl_getstate()
1034         return super(OptionDescription, self).__getstate__()
1035
1036
1037 def validate_requires_arg(requires, name):
1038     """check malformed requirements
1039     and tranform dict to internal tuple
1040
1041     :param requires: have a look at the
1042                      :meth:`tiramisu.setting.Settings.apply_requires` method to
1043                      know more about
1044                      the description of the requires dictionary
1045     """
1046     if requires is None:
1047         return None, None
1048     ret_requires = {}
1049     config_action = {}
1050
1051     # start parsing all requires given by user (has dict)
1052     # transforme it to a tuple
1053     for require in requires:
1054         if not type(require) == dict:
1055             raise ValueError(_("malformed requirements type for option:"
1056                                " {0}, must be a dict").format(name))
1057         valid_keys = ('option', 'expected', 'action', 'inverse', 'transitive',
1058                       'same_action')
1059         unknown_keys = frozenset(require.keys()) - frozenset(valid_keys)
1060         if unknown_keys != frozenset():
1061             raise ValueError('malformed requirements for option: {0}'
1062                              ' unknown keys {1}, must only '
1063                              '{2}'.format(name,
1064                                           unknown_keys,
1065                                           valid_keys))
1066         # prepare all attributes
1067         try:
1068             option = require['option']
1069             expected = require['expected']
1070             action = require['action']
1071         except KeyError:
1072             raise ValueError(_("malformed requirements for option: {0}"
1073                                " require must have option, expected and"
1074                                " action keys").format(name))
1075         inverse = require.get('inverse', False)
1076         if inverse not in [True, False]:
1077             raise ValueError(_('malformed requirements for option: {0}'
1078                                ' inverse must be boolean'))
1079         transitive = require.get('transitive', True)
1080         if transitive not in [True, False]:
1081             raise ValueError(_('malformed requirements for option: {0}'
1082                                ' transitive must be boolean'))
1083         same_action = require.get('same_action', True)
1084         if same_action not in [True, False]:
1085             raise ValueError(_('malformed requirements for option: {0}'
1086                                ' same_action must be boolean'))
1087
1088         if not isinstance(option, Option):
1089             raise ValueError(_('malformed requirements '
1090                                'must be an option in option {0}').format(name))
1091         if option.impl_is_multi():
1092             raise ValueError(_('malformed requirements option {0} '
1093                                'should not be a multi').format(name))
1094         if expected is not None:
1095             try:
1096                 option._validate(expected)
1097             except ValueError as err:
1098                 raise ValueError(_('malformed requirements second argument '
1099                                    'must be valid for option {0}'
1100                                    ': {1}').format(name, err))
1101         if action in config_action:
1102             if inverse != config_action[action]:
1103                 raise ValueError(_("inconsistency in action types"
1104                                    " for option: {0}"
1105                                    " action: {1}").format(name, action))
1106         else:
1107             config_action[action] = inverse
1108         if action not in ret_requires:
1109             ret_requires[action] = {}
1110         if option not in ret_requires[action]:
1111             ret_requires[action][option] = (option, [expected], action,
1112                                             inverse, transitive, same_action)
1113         else:
1114             ret_requires[action][option][1].append(expected)
1115     # transform dict to tuple
1116     ret = []
1117     for opt_requires in ret_requires.values():
1118         ret_action = []
1119         for require in opt_requires.values():
1120             ret_action.append((require[0], tuple(require[1]), require[2],
1121                                require[3], require[4], require[5]))
1122         ret.append(tuple(ret_action))
1123     return frozenset(config_action.keys()), tuple(ret)