error message for ip/broadcast/netmask/network validation
[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 import warnings
29
30 from tiramisu.error import ConflictError, ValueWarning
31 from tiramisu.setting import groups, multitypes
32 from tiramisu.i18n import _
33 from tiramisu.autolib import carry_out_calculation
34
35 name_regexp = re.compile(r'^\d+')
36 forbidden_names = ('iter_all', 'iter_group', 'find', 'find_first',
37                    'make_dict', 'unwrap_from_path', 'read_only',
38                    'read_write', 'getowner', 'set_contexts')
39
40
41 def valid_name(name):
42     "an option's name is a str and does not start with 'impl' or 'cfgimpl'"
43     try:
44         name = str(name)
45     except:
46         return False
47     if re.match(name_regexp, name) is None and not name.startswith('_') \
48             and name not in forbidden_names \
49             and not name.startswith('impl_') \
50             and not name.startswith('cfgimpl_'):
51         return True
52     else:
53         return False
54 #____________________________________________________________
55 #
56
57
58 class BaseOption(object):
59     """This abstract base class stands for attribute access
60     in options that have to be set only once, it is of course done in the
61     __setattr__ method
62     """
63     __slots__ = ('_name', '_requires', '_properties', '_readonly',
64                  '_consistencies', '_calc_properties', '_impl_informations',
65                  '_state_consistencies', '_state_readonly', '_state_requires',
66                  '_stated')
67
68     def __init__(self, name, doc, requires, properties):
69         if not valid_name(name):
70             raise ValueError(_("invalid name: {0} for option").format(name))
71         self._name = name
72         self._impl_informations = {}
73         self.impl_set_information('doc', doc)
74         self._calc_properties, self._requires = validate_requires_arg(
75             requires, self._name)
76         self._consistencies = None
77         if properties is None:
78             properties = tuple()
79         if not isinstance(properties, tuple):
80             raise TypeError(_('invalid properties type {0} for {1},'
81                             ' must be a tuple').format(
82                                 type(properties),
83                                 self._name))
84         if self._calc_properties is not None and properties is not tuple():
85             set_forbidden_properties = set(properties) & self._calc_properties
86             if set_forbidden_properties != frozenset():
87                 raise ValueError('conflict: properties already set in '
88                                  'requirement {0}'.format(
89                                      list(set_forbidden_properties)))
90         self._properties = properties  # 'hidden', 'disabled'...
91
92     def __setattr__(self, name, value):
93         """set once and only once some attributes in the option,
94         like `_name`. `_name` cannot be changed one the option and
95         pushed in the :class:`tiramisu.option.OptionDescription`.
96
97         if the attribute `_readonly` is set to `True`, the option is
98         "frozen" (which has noting to do with the high level "freeze"
99         propertie or "read_only" property)
100         """
101         if not name.startswith('_state') and name not in ('_cache_paths',
102                                                           '_consistencies'):
103             is_readonly = False
104             # never change _name
105             if name == '_name':
106                 try:
107                     self._name
108                     #so _name is already set
109                     is_readonly = True
110                 except:
111                     pass
112             try:
113                 if self._readonly is True:
114                     if value is True:
115                         # already readonly and try to re set readonly
116                         # don't raise, just exit
117                         return
118                     is_readonly = True
119             except AttributeError:
120                 pass
121             if is_readonly:
122                 raise AttributeError(_("'{0}' ({1}) object attribute '{2}' is"
123                                        " read-only").format(
124                                            self.__class__.__name__,
125                                            self._name,
126                                            name))
127         object.__setattr__(self, name, value)
128
129     # information
130     def impl_set_information(self, key, value):
131         """updates the information's attribute
132         (which is a dictionary)
133
134         :param key: information's key (ex: "help", "doc"
135         :param value: information's value (ex: "the help string")
136         """
137         self._impl_informations[key] = value
138
139     def impl_get_information(self, key, default=None):
140         """retrieves one information's item
141
142         :param key: the item string (ex: "help")
143         """
144         if key in self._impl_informations:
145             return self._impl_informations[key]
146         elif default is not None:
147             return default
148         else:
149             raise ValueError(_("information's item not found: {0}").format(
150                 key))
151
152     # serialize/unserialize
153     def _impl_convert_consistencies(self, descr, load=False):
154         """during serialization process, many things have to be done.
155         one of them is the localisation of the options.
156         The paths are set once for all.
157
158         :type descr: :class:`tiramisu.option.OptionDescription`
159         :param load: `True` if we are at the init of the option description
160         :type load: bool
161         """
162         if not load and self._consistencies is None:
163             self._state_consistencies = None
164         elif load and self._state_consistencies is None:
165             self._consistencies = None
166             del(self._state_consistencies)
167         else:
168             if load:
169                 consistencies = self._state_consistencies
170             else:
171                 consistencies = self._consistencies
172             if isinstance(consistencies, list):
173                 new_value = []
174                 for consistency in consistencies:
175                     if load:
176                         new_value.append((consistency[0],
177                                           descr.impl_get_opt_by_path(
178                                               consistency[1])))
179                     else:
180                         new_value.append((consistency[0],
181                                           descr.impl_get_path_by_opt(
182                                               consistency[1])))
183
184             else:
185                 new_value = {}
186                 for key, _consistencies in consistencies.items():
187                     new_value[key] = []
188                     for key_cons, _cons in _consistencies:
189                         _list_cons = []
190                         for _con in _cons:
191                             if load:
192                                 _list_cons.append(
193                                     descr.impl_get_opt_by_path(_con))
194                             else:
195                                 _list_cons.append(
196                                     descr.impl_get_path_by_opt(_con))
197                         new_value[key].append((key_cons, tuple(_list_cons)))
198             if load:
199                 del(self._state_consistencies)
200                 self._consistencies = new_value
201             else:
202                 self._state_consistencies = new_value
203
204     def _impl_convert_requires(self, descr, load=False):
205         """export of the requires during the serialization process
206
207         :type descr: :class:`tiramisu.option.OptionDescription`
208         :param load: `True` if we are at the init of the option description
209         :type load: bool
210         """
211         if not load and self._requires is None:
212             self._state_requires = None
213         elif load and self._state_requires is None:
214             self._requires = None
215             del(self._state_requires)
216         else:
217             if load:
218                 _requires = self._state_requires
219             else:
220                 _requires = self._requires
221             new_value = []
222             for requires in _requires:
223                 new_requires = []
224                 for require in requires:
225                     if load:
226                         new_require = [descr.impl_get_opt_by_path(require[0])]
227                     else:
228                         new_require = [descr.impl_get_path_by_opt(require[0])]
229                     new_require.extend(require[1:])
230                     new_requires.append(tuple(new_require))
231                 new_value.append(tuple(new_requires))
232             if load:
233                 del(self._state_requires)
234                 self._requires = new_value
235             else:
236                 self._state_requires = new_value
237
238     # serialize
239     def _impl_getstate(self, descr):
240         """the under the hood stuff that need to be done
241         before the serialization.
242
243         :param descr: the parent :class:`tiramisu.option.OptionDescription`
244         """
245         self._stated = True
246         for func in dir(self):
247             if func.startswith('_impl_convert_'):
248                 getattr(self, func)(descr)
249         try:
250             self._state_readonly = self._readonly
251         except AttributeError:
252             pass
253
254     def __getstate__(self, stated=True):
255         """special method to enable the serialization with pickle
256         Usualy, a `__getstate__` method does'nt need any parameter,
257         but somme under the hood stuff need to be done before this action
258
259         :parameter stated: if stated is `True`, the serialization protocol
260                            can be performed, not ready yet otherwise
261         :parameter type: bool
262         """
263         try:
264             self._stated
265         except AttributeError:
266             raise SystemError(_('cannot serialize Option, '
267                                 'only in OptionDescription'))
268         slots = set()
269         for subclass in self.__class__.__mro__:
270             if subclass is not object:
271                 slots.update(subclass.__slots__)
272         slots -= frozenset(['_cache_paths', '__weakref__'])
273         states = {}
274         for slot in slots:
275             # remove variable if save variable converted
276             # in _state_xxxx variable
277             if '_state' + slot not in slots:
278                 if slot.startswith('_state'):
279                     # should exists
280                     states[slot] = getattr(self, slot)
281                     # remove _state_xxx variable
282                     self.__delattr__(slot)
283                 else:
284                     try:
285                         states[slot] = getattr(self, slot)
286                     except AttributeError:
287                         pass
288         if not stated:
289             del(states['_stated'])
290         return states
291
292     # unserialize
293     def _impl_setstate(self, descr):
294         """the under the hood stuff that need to be done
295         before the serialization.
296
297         :type descr: :class:`tiramisu.option.OptionDescription`
298         """
299         for func in dir(self):
300             if func.startswith('_impl_convert_'):
301                 getattr(self, func)(descr, load=True)
302         try:
303             self._readonly = self._state_readonly
304             del(self._state_readonly)
305             del(self._stated)
306         except AttributeError:
307             pass
308
309     def __setstate__(self, state):
310         """special method that enables us to serialize (pickle)
311
312         Usualy, a `__setstate__` method does'nt need any parameter,
313         but somme under the hood stuff need to be done before this action
314
315         :parameter state: a dict is passed to the loads, it is the attributes
316                           of the options object
317         :type state: dict
318         """
319         for key, value in state.items():
320             setattr(self, key, value)
321
322
323 class Option(BaseOption):
324     """
325     Abstract base class for configuration option's.
326
327     Reminder: an Option object is **not** a container for the value.
328     """
329     __slots__ = ('_multi', '_validator', '_default_multi', '_default',
330                  '_state_callback', '_callback', '_multitype',
331                  '_only_warning', '_master_slaves', '__weakref__')
332     _empty = ''
333
334     def __init__(self, name, doc, default=None, default_multi=None,
335                  requires=None, multi=False, callback=None,
336                  callback_params=None, validator=None, validator_params=None,
337                  properties=None, only_warning=False):
338         """
339         :param name: the option's name
340         :param doc: the option's description
341         :param default: specifies the default value of the option,
342                         for a multi : ['bla', 'bla', 'bla']
343         :param default_multi: 'bla' (used in case of a reset to default only at
344                         a given index)
345         :param requires: is a list of names of options located anywhere
346                          in the configuration.
347         :param multi: if true, the option's value is a list
348         :param callback: the name of a function. If set, the function's output
349                          is responsible of the option's value
350         :param callback_params: the callback's parameter
351         :param validator: the name of a function which stands for a custom
352                           validation of the value
353         :param validator_params: the validator's parameters
354         :param properties: tuple of default properties
355         :param only_warning: _validator and _consistencies don't raise if True
356                              Values()._warning contain message
357
358         """
359         super(Option, self).__init__(name, doc, requires, properties)
360         self._multi = multi
361         if validator is not None:
362             validate_callback(validator, validator_params, 'validator')
363             self._validator = (validator, validator_params)
364         else:
365             self._validator = None
366         if not self._multi and default_multi is not None:
367             raise ValueError(_("a default_multi is set whereas multi is False"
368                              " in option: {0}").format(name))
369         if default_multi is not None:
370             try:
371                 self._validate(default_multi)
372             except ValueError as err:
373                 raise ValueError(_("invalid default_multi value {0} "
374                                    "for option {1}: {2}").format(
375                                        str(default_multi), name, err))
376         if callback is not None and (default is not None or
377                                      default_multi is not None):
378             raise ValueError(_("default value not allowed if option: {0} "
379                              "is calculated").format(name))
380         if callback is None and callback_params is not None:
381             raise ValueError(_("params defined for a callback function but "
382                              "no callback defined"
383                              " yet for option {0}").format(name))
384         if callback is not None:
385             validate_callback(callback, callback_params, 'callback')
386             self._callback = (callback, callback_params)
387         else:
388             self._callback = None
389         if self._multi:
390             if default is None:
391                 default = []
392             self._multitype = multitypes.default
393             self._default_multi = default_multi
394         self._only_warning = only_warning
395         self.impl_validate(default)
396         self._default = default
397
398     def _launch_consistency(self, func, opt, vals, context, index, opt_):
399         if context is not None:
400             descr = context.cfgimpl_get_description()
401         if opt is self:
402             #values are for self, search opt_ values
403             values = vals
404             if context is not None:
405                 path = descr.impl_get_path_by_opt(opt_)
406                 values_ = context._getattr(path, validate=False)
407             else:
408                 values_ = opt_.impl_getdefault()
409             if index is not None:
410                 #value is not already set, could be higher
411                 try:
412                     values_ = values_[index]
413                 except IndexError:
414                     values_ = None
415         else:
416             #values are for opt_, search self values
417             values_ = vals
418             if context is not None:
419                 path = descr.impl_get_path_by_opt(self)
420                 values = context._getattr(path, validate=False)
421             else:
422                 values = self.impl_getdefault()
423             if index is not None:
424                 #value is not already set, could be higher
425                 try:
426                     values = values[index]
427                 except IndexError:
428                     values = None
429         if index is None and self.impl_is_multi():
430             for index in range(0, len(values)):
431                 try:
432                     value = values[index]
433                     value_ = values_[index]
434                 except IndexError:
435                     value = None
436                     value_ = None
437                 if None not in (value, value_):
438                     getattr(self, func)(opt_._name, value, value_)
439         else:
440             if None not in (values, values_):
441                 getattr(self, func)(opt_._name, values, values_)
442
443     def impl_validate(self, value, context=None, validate=True,
444                       force_no_multi=False):
445         """
446         :param value: the option's value
447         :param context: Config's context
448         :type context: :class:`tiramisu.config.Config`
449         :param validate: if true enables ``self._validator`` validation
450         :type validate: boolean
451         :param force_no_multi: if multi, value has to be a list
452                                not if force_no_multi is True
453         :type force_no_multi: boolean
454         """
455         if not validate:
456             return
457
458         def val_validator(val):
459             if self._validator is not None:
460                 if self._validator[1] is not None:
461                     validator_params = deepcopy(self._validator[1])
462                     if '' in validator_params:
463                         lst = list(validator_params[''])
464                         lst.insert(0, val)
465                         validator_params[''] = tuple(lst)
466                     else:
467                         validator_params[''] = (val,)
468                 else:
469                     validator_params = {'': (val,)}
470                 # Raise ValueError if not valid
471                 carry_out_calculation(self._name, config=context,
472                                       callback=self._validator[0],
473                                       callback_params=validator_params)
474
475         def do_validation(_value, _index=None):
476             if _value is None:
477                 return
478             try:
479                 # valid with self._validator
480                 val_validator(_value)
481                 # if not context launch consistency validation
482                 if context is not None:
483                     descr._valid_consistency(self, _value, context, _index)
484                 self._second_level_validation(_value)
485             except ValueError as err:
486                 msg = _("invalid value {0} for option {1}: {2}").format(
487                     _value, self._name, err)
488                 if self._only_warning:
489                     warnings.warn_explicit(ValueWarning(msg, self),
490                                            ValueWarning,
491                                            self.__class__.__name__, 0)
492                 else:
493                     raise ValueError(msg)
494             # option validation
495             self._validate(_value)
496
497         # generic calculation
498         if context is not None:
499             descr = context.cfgimpl_get_description()
500
501         ret = None
502         if not self._multi or force_no_multi:
503             ret = do_validation(value)
504         else:
505             if not isinstance(value, list):
506                 raise ValueError(_("invalid value {0} for option {1} "
507                                    "which must be a list").format(value,
508                                                                   self._name))
509             for index, val in enumerate(value):
510                 ret_ = do_validation(val, index)
511                 if ret_ is not None:
512                     ret = ret_
513         return ret
514
515     def impl_getdefault(self, default_multi=False):
516         "accessing the default value"
517         if not default_multi or not self.impl_is_multi():
518             return self._default
519         else:
520             return self.getdefault_multi()
521
522     def impl_getdefault_multi(self):
523         "accessing the default value for a multi"
524         return self._default_multi
525
526     def impl_get_multitype(self):
527         return self._multitype
528
529     def impl_get_master_slaves(self):
530         return self._master_slaves
531
532     def impl_is_empty_by_default(self):
533         "no default value has been set yet"
534         if ((not self.impl_is_multi() and self._default is None) or
535                 (self.impl_is_multi() and (self._default == []
536                                            or None in self._default))):
537             return True
538         return False
539
540     def impl_getdoc(self):
541         "accesses the Option's doc"
542         return self.impl_get_information('doc')
543
544     def impl_has_callback(self):
545         "to know if a callback has been defined or not"
546         if self._callback is None:
547             return False
548         else:
549             return True
550
551     def impl_getkey(self, value):
552         return value
553
554     def impl_is_multi(self):
555         return self._multi
556
557     def impl_add_consistency(self, func, opt):
558         if self._consistencies is None:
559             self._consistencies = []
560         if not isinstance(opt, Option):
561             raise ValueError('consistency must be set with an option')
562         if self is opt:
563             raise ValueError('cannot add consistency with itself')
564         if self.impl_is_multi() != opt.impl_is_multi():
565             raise ValueError('options in consistency'
566                              ' should be multi in two sides')
567         func = '_cons_{0}'.format(func)
568         self._launch_consistency(func,
569                                  self,
570                                  self.impl_getdefault(),
571                                  None, None, opt)
572         self._consistencies.append((func, opt))
573         self.impl_validate(self.impl_getdefault())
574
575     def _cons_not_equal(self, optname, value, value_):
576         if value == value_:
577             raise ValueError(_("invalid value {0} for option {1} "
578                                "must be different as {2} option"
579                                "").format(value, self._name, optname))
580
581     def _impl_convert_callbacks(self, descr, load=False):
582         if not load and self._callback is None:
583             self._state_callback = None
584         elif load and self._state_callback is None:
585             self._callback = None
586             del(self._state_callback)
587         else:
588             if load:
589                 callback, callback_params = self._state_callback
590             else:
591                 callback, callback_params = self._callback
592             if callback_params is not None:
593                 cllbck_prms = {}
594                 for key, values in callback_params.items():
595                     vls = []
596                     for value in values:
597                         if isinstance(value, tuple):
598                             if load:
599                                 value = (descr.impl_get_opt_by_path(value[0]),
600                                          value[1])
601                             else:
602                                 value = (descr.impl_get_path_by_opt(value[0]),
603                                          value[1])
604                         vls.append(value)
605                     cllbck_prms[key] = tuple(vls)
606             else:
607                 cllbck_prms = None
608
609             if load:
610                 del(self._state_callback)
611                 self._callback = (callback, cllbck_prms)
612             else:
613                 self._state_callback = (callback, cllbck_prms)
614
615     def _second_level_validation(self, value):
616         pass
617
618
619 class ChoiceOption(Option):
620     """represents a choice out of several objects.
621
622     The option can also have the value ``None``
623     """
624
625     __slots__ = ('_values', '_open_values')
626     _opt_type = 'string'
627
628     def __init__(self, name, doc, values, default=None, default_multi=None,
629                  requires=None, multi=False, callback=None,
630                  callback_params=None, open_values=False, validator=None,
631                  validator_params=None, properties=None, only_warning=False):
632         """
633         :param values: is a list of values the option can possibly take
634         """
635         if not isinstance(values, tuple):
636             raise TypeError(_('values must be a tuple for {0}').format(name))
637         self._values = values
638         if open_values not in (True, False):
639             raise TypeError(_('open_values must be a boolean for '
640                             '{0}').format(name))
641         self._open_values = open_values
642         super(ChoiceOption, self).__init__(name, doc, default=default,
643                                            default_multi=default_multi,
644                                            callback=callback,
645                                            callback_params=callback_params,
646                                            requires=requires,
647                                            multi=multi,
648                                            validator=validator,
649                                            validator_params=validator_params,
650                                            properties=properties,
651                                            only_warning=only_warning)
652
653     def impl_get_values(self):
654         return self._values
655
656     def impl_is_openvalues(self):
657         return self._open_values
658
659     def _validate(self, value):
660         if not self._open_values and not value in self._values:
661             raise ValueError(_('value {0} is not permitted, '
662                                'only {1} is allowed'
663                                '').format(value, self._values))
664
665
666 class BoolOption(Option):
667     "represents a choice between ``True`` and ``False``"
668     __slots__ = tuple()
669     _opt_type = 'bool'
670
671     def _validate(self, value):
672         if not isinstance(value, bool):
673             raise ValueError(_('value must be a boolean'))
674
675
676 class IntOption(Option):
677     "represents a choice of an integer"
678     __slots__ = tuple()
679     _opt_type = 'int'
680
681     def _validate(self, value):
682         if not isinstance(value, int):
683             raise ValueError(_('value must be an integer'))
684
685
686 class FloatOption(Option):
687     "represents a choice of a floating point number"
688     __slots__ = tuple()
689     _opt_type = 'float'
690
691     def _validate(self, value):
692         if not isinstance(value, float):
693             raise ValueError(_('value must be a float'))
694
695
696 class StrOption(Option):
697     "represents the choice of a string"
698     __slots__ = tuple()
699     _opt_type = 'string'
700
701     def _validate(self, value):
702         if not isinstance(value, str):
703             raise ValueError(_('value must be a string, not '
704                                '{0}').format(type(value)))
705
706
707 if sys.version_info[0] >= 3:
708     #UnicodeOption is same has StrOption in python 3+
709     class UnicodeOption(StrOption):
710         __slots__ = tuple()
711         pass
712 else:
713     class UnicodeOption(Option):
714         "represents the choice of a unicode string"
715         __slots__ = tuple()
716         _opt_type = 'unicode'
717         _empty = u''
718
719         def _validate(self, value):
720             if not isinstance(value, unicode):
721                 raise ValueError(_('value must be an unicode'))
722
723
724 class SymLinkOption(BaseOption):
725     __slots__ = ('_name', '_opt', '_state_opt')
726     _opt_type = 'symlink'
727     #not return _opt consistencies
728     _consistencies = {}
729
730     def __init__(self, name, opt):
731         self._name = name
732         if not isinstance(opt, Option):
733             raise ValueError(_('malformed symlinkoption '
734                                'must be an option '
735                                'for symlink {0}').format(name))
736         self._opt = opt
737         self._readonly = True
738
739     def __getattr__(self, name):
740         if name in ('_name', '_opt', '_opt_type', '_readonly'):
741             return object.__getattr__(self, name)
742         else:
743             return getattr(self._opt, name)
744
745     def _impl_getstate(self, descr):
746         super(SymLinkOption, self)._impl_getstate(descr)
747         self._state_opt = descr.impl_get_path_by_opt(self._opt)
748
749     def _impl_setstate(self, descr):
750         self._opt = descr.impl_get_opt_by_path(self._state_opt)
751         del(self._state_opt)
752         super(SymLinkOption, self)._impl_setstate(descr)
753
754     def _impl_convert_consistencies(self, descr, load=False):
755         if load:
756             del(self._state_consistencies)
757         else:
758             self._state_consistencies = None
759
760
761 class IPOption(Option):
762     "represents the choice of an ip"
763     __slots__ = ('_only_private', '_allow_reserved')
764     _opt_type = 'ip'
765
766     def __init__(self, name, doc, default=None, default_multi=None,
767                  requires=None, multi=False, callback=None,
768                  callback_params=None, validator=None, validator_params=None,
769                  properties=None, only_private=False, allow_reserved=False,
770                  only_warning=False):
771         self._only_private = only_private
772         self._allow_reserved = allow_reserved
773         super(IPOption, self).__init__(name, doc, default=default,
774                                        default_multi=default_multi,
775                                        callback=callback,
776                                        callback_params=callback_params,
777                                        requires=requires,
778                                        multi=multi,
779                                        validator=validator,
780                                        validator_params=validator_params,
781                                        properties=properties,
782                                        only_warning=only_warning)
783
784     def _validate(self, value):
785         try:
786             IP('{0}/32'.format(value))
787         except ValueError:
788             raise ValueError(_('invalid IP {0}').format(self._name))
789
790     def _second_level_validation(self, value):
791         ip = IP('{0}/32'.format(value))
792         if not self._allow_reserved and ip.iptype() == 'RESERVED':
793             raise ValueError(_("IP mustn't not be in reserved class"))
794         if self._only_private and not ip.iptype() == 'PRIVATE':
795             raise ValueError(_("IP must be in private class"))
796
797
798 class PortOption(Option):
799     """represents the choice of a port
800     The port numbers are divided into three ranges:
801     the well-known ports,
802     the registered ports,
803     and the dynamic or private ports.
804     You can actived this three range.
805     Port number 0 is reserved and can't be used.
806     see: http://en.wikipedia.org/wiki/Port_numbers
807     """
808     __slots__ = ('_allow_range', '_allow_zero', '_min_value', '_max_value')
809     _opt_type = 'port'
810
811     def __init__(self, name, doc, default=None, default_multi=None,
812                  requires=None, multi=False, callback=None,
813                  callback_params=None, validator=None, validator_params=None,
814                  properties=None, allow_range=False, allow_zero=False,
815                  allow_wellknown=True, allow_registred=True,
816                  allow_private=False, only_warning=False):
817         self._allow_range = allow_range
818         self._min_value = None
819         self._max_value = None
820         ports_min = [0, 1, 1024, 49152]
821         ports_max = [0, 1023, 49151, 65535]
822         is_finally = False
823         for index, allowed in enumerate([allow_zero,
824                                          allow_wellknown,
825                                          allow_registred,
826                                          allow_private]):
827             if self._min_value is None:
828                 if allowed:
829                     self._min_value = ports_min[index]
830             elif not allowed:
831                 is_finally = True
832             elif allowed and is_finally:
833                 raise ValueError(_('inconsistency in allowed range'))
834             if allowed:
835                 self._max_value = ports_max[index]
836
837         if self._max_value is None:
838             raise ValueError(_('max value is empty'))
839
840         super(PortOption, self).__init__(name, doc, default=default,
841                                          default_multi=default_multi,
842                                          callback=callback,
843                                          callback_params=callback_params,
844                                          requires=requires,
845                                          multi=multi,
846                                          validator=validator,
847                                          validator_params=validator_params,
848                                          properties=properties,
849                                          only_warning=only_warning)
850
851     def _validate(self, value):
852         if self._allow_range and ":" in str(value):
853             value = str(value).split(':')
854             if len(value) != 2:
855                 raise ValueError('range must have two values only')
856             if not value[0] < value[1]:
857                 raise ValueError('first port in range must be'
858                                  ' smaller than the second one')
859         else:
860             value = [value]
861
862         for val in value:
863             if not self._min_value <= int(val) <= self._max_value:
864                 raise ValueError('port must be an between {0} and {1}'
865                                  ''.format(self._min_value, self._max_value))
866
867
868 class NetworkOption(Option):
869     "represents the choice of a network"
870     __slots__ = tuple()
871     _opt_type = 'network'
872
873     def _validate(self, value):
874         try:
875             IP(value)
876         except ValueError:
877             raise ValueError(_('invalid network address {0}').format(self._name))
878
879     def _second_level_validation(self, value):
880         ip = IP(value)
881         if ip.iptype() == 'RESERVED':
882             raise ValueError(_("network shall not be in reserved class"))
883
884
885 class NetmaskOption(Option):
886     "represents the choice of a netmask"
887     __slots__ = tuple()
888     _opt_type = 'netmask'
889
890     def _validate(self, value):
891         try:
892             IP('0.0.0.0/{0}'.format(value))
893         except ValueError:
894             raise ValueError(_('invalid netmask address {0}').format(self._name))
895
896     def _cons_network_netmask(self, optname, value, value_):
897         #opts must be (netmask, network) options
898         self.__cons_netmask(optname, value, value_, False)
899
900     def _cons_ip_netmask(self, optname, value, value_):
901         #opts must be (netmask, ip) options
902         self.__cons_netmask(optname, value, value_, True)
903
904     def __cons_netmask(self, optname, val_netmask, val_ipnetwork, make_net):
905         msg = None
906         try:
907             ip = IP('{0}/{1}'.format(val_ipnetwork, val_netmask),
908                     make_net=make_net)
909             #if cidr == 32, ip same has network
910             if ip.prefixlen() != 32:
911                 try:
912                     IP('{0}/{1}'.format(val_ipnetwork, val_netmask),
913                         make_net=not make_net)
914                 except ValueError:
915                     if not make_net:
916                         msg = _("invalid network {0} ({1}) "
917                                 "with netmask {2} ({3}),"
918                                 " this network is an IP")
919                 else:
920                     if make_net:
921                         msg = _("invalid IP {0} ({1}) with netmask {2} ({3}),"
922                                 " this IP is a network")
923
924         except ValueError:
925             if make_net:
926                 msg = _("invalid IP {0} ({1}) with netmask {2} ({3})")
927             else:
928                 msg = _("invalid network {0} ({1}) with netmask {2} ({3})")
929         if msg is not None:
930             raise ValueError(msg.format(val_ipnetwork, optname,
931                                         val_netmask, self._name))
932
933
934 class BroadcastOption(Option):
935     def _validate(self, value):
936         try:
937             IP('{0}/32'.format(value))
938         except ValueError:
939             raise ValueError(_('invalid broadcast address {0}').format(self._name))
940
941
942 class DomainnameOption(Option):
943     "represents the choice of a domain name"
944     __slots__ = ('_type', '_allow_ip')
945     _opt_type = 'domainname'
946
947     def __init__(self, name, doc, default=None, default_multi=None,
948                  requires=None, multi=False, callback=None,
949                  callback_params=None, validator=None, validator_params=None,
950                  properties=None, allow_ip=False, type_='domainname',
951                  only_warning=False):
952         #netbios: for MS domain
953         #hostname: to identify the device
954         #domainname:
955         #fqdn: with tld, not supported yet
956         if type_ not in ['netbios', 'hostname', 'domainname']:
957             raise ValueError(_('unknown type_ {0} for hostname').format(type_))
958         self._type = type_
959         if allow_ip not in [True, False]:
960             raise ValueError(_('allow_ip must be a boolean'))
961         self._allow_ip = allow_ip
962         super(DomainnameOption, self).__init__(name, doc, default=default,
963                                                default_multi=default_multi,
964                                                callback=callback,
965                                                callback_params=callback_params,
966                                                requires=requires,
967                                                multi=multi,
968                                                validator=validator,
969                                                validator_params=validator_params,
970                                                properties=properties,
971                                                only_warning=only_warning)
972
973     def _validate(self, value):
974         if self._allow_ip is True:
975             try:
976                 IP('{0}/32'.format(value))
977                 return
978             except ValueError:
979                 pass
980         if self._type == 'netbios':
981             length = 15
982             extrachar = ''
983         elif self._type == 'hostname':
984             length = 63
985             extrachar = ''
986         elif self._type == 'domainname':
987             length = 255
988             extrachar = '\.'
989             if '.' not in value:
990                 raise ValueError(_("invalid value for {0}, must have dot"
991                                    "").format(self._name))
992         if len(value) > length:
993             raise ValueError(_("invalid domainname's length for"
994                                " {0} (max {1})").format(self._name, length))
995         if len(value) == 1:
996             raise ValueError(_("invalid domainname's length for {0} (min 2)"
997                                "").format(self._name))
998         regexp = r'^[a-z]([a-z\d{0}-])*[a-z\d]$'.format(extrachar)
999         if re.match(regexp, value) is None:
1000             raise ValueError(_('invalid domainname'))
1001
1002
1003 class OptionDescription(BaseOption):
1004     """Config's schema (organisation, group) and container of Options
1005     The `OptionsDescription` objects lives in the `tiramisu.config.Config`.
1006     """
1007     __slots__ = ('_name', '_requires', '_cache_paths', '_group_type',
1008                  '_state_group_type', '_properties', '_children',
1009                  '_consistencies', '_calc_properties', '__weakref__',
1010                  '_readonly', '_impl_informations', '_state_requires',
1011                  '_state_consistencies', '_stated', '_state_readonly')
1012     _opt_type = 'optiondescription'
1013
1014     def __init__(self, name, doc, children, requires=None, properties=None):
1015         """
1016         :param children: a list of options (including optiondescriptions)
1017
1018         """
1019         super(OptionDescription, self).__init__(name, doc, requires, properties)
1020         child_names = [child._name for child in children]
1021         #better performance like this
1022         valid_child = copy(child_names)
1023         valid_child.sort()
1024         old = None
1025         for child in valid_child:
1026             if child == old:
1027                 raise ConflictError(_('duplicate option name: '
1028                                       '{0}').format(child))
1029             old = child
1030         self._children = (tuple(child_names), tuple(children))
1031         self._cache_paths = None
1032         # the group_type is useful for filtering OptionDescriptions in a config
1033         self._group_type = groups.default
1034
1035     def impl_getdoc(self):
1036         return self.impl_get_information('doc')
1037
1038     def __getattr__(self, name):
1039         if name in self.__slots__:
1040             return object.__getattribute__(self, name)
1041         try:
1042             return self._children[1][self._children[0].index(name)]
1043         except ValueError:
1044             raise AttributeError(_('unknown Option {0} '
1045                                    'in OptionDescription {1}'
1046                                    '').format(name, self._name))
1047
1048     def impl_getkey(self, config):
1049         return tuple([child.impl_getkey(getattr(config, child._name))
1050                       for child in self.impl_getchildren()])
1051
1052     def impl_getpaths(self, include_groups=False, _currpath=None):
1053         """returns a list of all paths in self, recursively
1054            _currpath should not be provided (helps with recursion)
1055         """
1056         if _currpath is None:
1057             _currpath = []
1058         paths = []
1059         for option in self.impl_getchildren():
1060             attr = option._name
1061             if isinstance(option, OptionDescription):
1062                 if include_groups:
1063                     paths.append('.'.join(_currpath + [attr]))
1064                 paths += option.impl_getpaths(include_groups=include_groups,
1065                                               _currpath=_currpath + [attr])
1066             else:
1067                 paths.append('.'.join(_currpath + [attr]))
1068         return paths
1069
1070     def impl_getchildren(self):
1071         return self._children[1]
1072
1073     def impl_build_cache(self,
1074                          cache_path=None,
1075                          cache_option=None,
1076                          _currpath=None,
1077                          _consistencies=None,
1078                          force_no_consistencies=False):
1079         if _currpath is None and self._cache_paths is not None:
1080             # cache already set
1081             return
1082         if _currpath is None:
1083             save = True
1084             _currpath = []
1085             if not force_no_consistencies:
1086                 _consistencies = {}
1087         else:
1088             save = False
1089         if cache_path is None:
1090             cache_path = []
1091             cache_option = []
1092         for option in self.impl_getchildren():
1093             attr = option._name
1094             if option in cache_option:
1095                 raise ConflictError(_('duplicate option: {0}').format(option))
1096
1097             cache_option.append(option)
1098             if not force_no_consistencies:
1099                 option._readonly = True
1100             cache_path.append(str('.'.join(_currpath + [attr])))
1101             if not isinstance(option, OptionDescription):
1102                 if not force_no_consistencies and \
1103                         option._consistencies is not None:
1104                     for consistency in option._consistencies:
1105                         func, opt = consistency
1106                         opts = (option, opt)
1107                         _consistencies.setdefault(opt,
1108                                                   []).append((func, opts))
1109                         _consistencies.setdefault(option,
1110                                                   []).append((func, opts))
1111             else:
1112                 _currpath.append(attr)
1113                 option.impl_build_cache(cache_path,
1114                                         cache_option,
1115                                         _currpath,
1116                                         _consistencies,
1117                                         force_no_consistencies)
1118                 _currpath.pop()
1119         if save:
1120             self._cache_paths = (tuple(cache_option), tuple(cache_path))
1121             if not force_no_consistencies:
1122                 self._consistencies = _consistencies
1123                 self._readonly = True
1124
1125     def impl_get_opt_by_path(self, path):
1126         try:
1127             return self._cache_paths[0][self._cache_paths[1].index(path)]
1128         except ValueError:
1129             raise AttributeError(_('no option for path {0}').format(path))
1130
1131     def impl_get_path_by_opt(self, opt):
1132         try:
1133             return self._cache_paths[1][self._cache_paths[0].index(opt)]
1134         except ValueError:
1135             raise AttributeError(_('no option {0} found').format(opt))
1136
1137     # ____________________________________________________________
1138     def impl_set_group_type(self, group_type):
1139         """sets a given group object to an OptionDescription
1140
1141         :param group_type: an instance of `GroupType` or `MasterGroupType`
1142                               that lives in `setting.groups`
1143         """
1144         if self._group_type != groups.default:
1145             raise TypeError(_('cannot change group_type if already set '
1146                             '(old {0}, new {1})').format(self._group_type,
1147                                                          group_type))
1148         if isinstance(group_type, groups.GroupType):
1149             self._group_type = group_type
1150             if isinstance(group_type, groups.MasterGroupType):
1151                 #if master (same name has group) is set
1152                 identical_master_child_name = False
1153                 #for collect all slaves
1154                 slaves = []
1155                 master = None
1156                 for child in self.impl_getchildren():
1157                     if isinstance(child, OptionDescription):
1158                         raise ValueError(_("master group {0} shall not have "
1159                                          "a subgroup").format(self._name))
1160                     if isinstance(child, SymLinkOption):
1161                         raise ValueError(_("master group {0} shall not have "
1162                                          "a symlinkoption").format(self._name))
1163                     if not child.impl_is_multi():
1164                         raise ValueError(_("not allowed option {0} "
1165                                          "in group {1}"
1166                                          ": this option is not a multi"
1167                                          "").format(child._name, self._name))
1168                     if child._name == self._name:
1169                         identical_master_child_name = True
1170                         child._multitype = multitypes.master
1171                         master = child
1172                     else:
1173                         slaves.append(child)
1174                 if master is None:
1175                     raise ValueError(_('master group with wrong'
1176                                        ' master name for {0}'
1177                                        ).format(self._name))
1178                 master._master_slaves = tuple(slaves)
1179                 for child in self.impl_getchildren():
1180                     if child != master:
1181                         child._master_slaves = master
1182                         child._multitype = multitypes.slave
1183                 if not identical_master_child_name:
1184                     raise ValueError(_("no child has same nom has master group"
1185                                        " for: {0}").format(self._name))
1186         else:
1187             raise ValueError(_('group_type: {0}'
1188                                ' not allowed').format(group_type))
1189
1190     def impl_get_group_type(self):
1191         return self._group_type
1192
1193     def _valid_consistency(self, opt, value, context=None, index=None):
1194         consistencies = self._consistencies.get(opt)
1195         if consistencies is not None:
1196             for consistency in consistencies:
1197                 opt_ = consistency[1]
1198                 ret = opt_[0]._launch_consistency(consistency[0],
1199                                                   opt,
1200                                                   value,
1201                                                   context,
1202                                                   index,
1203                                                   opt_[1])
1204                 if ret is False:
1205                     return False
1206         return True
1207
1208     def _impl_getstate(self, descr=None):
1209         """enables us to export into a dict
1210         :param descr: parent :class:`tiramisu.option.OptionDescription`
1211         """
1212         if descr is None:
1213             self.impl_build_cache()
1214             descr = self
1215         super(OptionDescription, self)._impl_getstate(descr)
1216         self._state_group_type = str(self._group_type)
1217         for option in self.impl_getchildren():
1218             option._impl_getstate(descr)
1219
1220     def __getstate__(self):
1221         """special method to enable the serialization with pickle
1222         """
1223         stated = True
1224         try:
1225             # the `_state` attribute is a flag that which tells us if
1226             # the serialization can be performed
1227             self._stated
1228         except AttributeError:
1229             # if cannot delete, _impl_getstate never launch
1230             # launch it recursivement
1231             # _stated prevent __getstate__ launch more than one time
1232             # _stated is delete, if re-serialize, re-lauch _impl_getstate
1233             self._impl_getstate()
1234             stated = False
1235         return super(OptionDescription, self).__getstate__(stated)
1236
1237     def _impl_setstate(self, descr=None):
1238         """enables us to import from a dict
1239         :param descr: parent :class:`tiramisu.option.OptionDescription`
1240         """
1241         if descr is None:
1242             self._cache_paths = None
1243             self.impl_build_cache(force_no_consistencies=True)
1244             descr = self
1245         self._group_type = getattr(groups, self._state_group_type)
1246         del(self._state_group_type)
1247         super(OptionDescription, self)._impl_setstate(descr)
1248         for option in self.impl_getchildren():
1249             option._impl_setstate(descr)
1250
1251     def __setstate__(self, state):
1252         super(OptionDescription, self).__setstate__(state)
1253         try:
1254             self._stated
1255         except AttributeError:
1256             self._impl_setstate()
1257
1258
1259 def validate_requires_arg(requires, name):
1260     """check malformed requirements
1261     and tranform dict to internal tuple
1262
1263     :param requires: have a look at the
1264                      :meth:`tiramisu.setting.Settings.apply_requires` method to
1265                      know more about
1266                      the description of the requires dictionary
1267     """
1268     if requires is None:
1269         return None, None
1270     ret_requires = {}
1271     config_action = {}
1272
1273     # start parsing all requires given by user (has dict)
1274     # transforme it to a tuple
1275     for require in requires:
1276         if not type(require) == dict:
1277             raise ValueError(_("malformed requirements type for option:"
1278                                " {0}, must be a dict").format(name))
1279         valid_keys = ('option', 'expected', 'action', 'inverse', 'transitive',
1280                       'same_action')
1281         unknown_keys = frozenset(require.keys()) - frozenset(valid_keys)
1282         if unknown_keys != frozenset():
1283             raise ValueError('malformed requirements for option: {0}'
1284                              ' unknown keys {1}, must only '
1285                              '{2}'.format(name,
1286                                           unknown_keys,
1287                                           valid_keys))
1288         # prepare all attributes
1289         try:
1290             option = require['option']
1291             expected = require['expected']
1292             action = require['action']
1293         except KeyError:
1294             raise ValueError(_("malformed requirements for option: {0}"
1295                                " require must have option, expected and"
1296                                " action keys").format(name))
1297         inverse = require.get('inverse', False)
1298         if inverse not in [True, False]:
1299             raise ValueError(_('malformed requirements for option: {0}'
1300                                ' inverse must be boolean'))
1301         transitive = require.get('transitive', True)
1302         if transitive not in [True, False]:
1303             raise ValueError(_('malformed requirements for option: {0}'
1304                                ' transitive must be boolean'))
1305         same_action = require.get('same_action', True)
1306         if same_action not in [True, False]:
1307             raise ValueError(_('malformed requirements for option: {0}'
1308                                ' same_action must be boolean'))
1309
1310         if not isinstance(option, Option):
1311             raise ValueError(_('malformed requirements '
1312                                'must be an option in option {0}').format(name))
1313         if option.impl_is_multi():
1314             raise ValueError(_('malformed requirements option {0} '
1315                                'should not be a multi').format(name))
1316         if expected is not None:
1317             try:
1318                 option._validate(expected)
1319             except ValueError as err:
1320                 raise ValueError(_('malformed requirements second argument '
1321                                    'must be valid for option {0}'
1322                                    ': {1}').format(name, err))
1323         if action in config_action:
1324             if inverse != config_action[action]:
1325                 raise ValueError(_("inconsistency in action types"
1326                                    " for option: {0}"
1327                                    " action: {1}").format(name, action))
1328         else:
1329             config_action[action] = inverse
1330         if action not in ret_requires:
1331             ret_requires[action] = {}
1332         if option not in ret_requires[action]:
1333             ret_requires[action][option] = (option, [expected], action,
1334                                             inverse, transitive, same_action)
1335         else:
1336             ret_requires[action][option][1].append(expected)
1337     # transform dict to tuple
1338     ret = []
1339     for opt_requires in ret_requires.values():
1340         ret_action = []
1341         for require in opt_requires.values():
1342             ret_action.append((require[0], tuple(require[1]), require[2],
1343                                require[3], require[4], require[5]))
1344         ret.append(tuple(ret_action))
1345     return frozenset(config_action.keys()), tuple(ret)
1346
1347
1348 def validate_callback(callback, callback_params, type_):
1349     if type(callback) != FunctionType:
1350         raise ValueError(_('{0} should be a function').format(type_))
1351     if callback_params is not None:
1352         if not isinstance(callback_params, dict):
1353             raise ValueError(_('{0}_params should be a dict').format(type_))
1354         for key, callbacks in callback_params.items():
1355             if key != '' and len(callbacks) != 1:
1356                 raise ValueError(_('{0}_params with key {1} should not have '
1357                                    'length different to 1').format(type_,
1358                                                                    key))
1359             if not isinstance(callbacks, tuple):
1360                 raise ValueError(_('{0}_params should be tuple for key "{1}"'
1361                                    ).format(type_, key))
1362             for callbk in callbacks:
1363                 if isinstance(callbk, tuple):
1364                     option, force_permissive = callbk
1365                     if type_ == 'validator' and not force_permissive:
1366                         raise ValueError(_('validator not support tuple'))
1367                     if not isinstance(option, Option) and not \
1368                             isinstance(option, SymLinkOption):
1369                         raise ValueError(_('{0}_params should have an option '
1370                                            'not a {0} for first argument'
1371                                            ).format(type_, type(option)))
1372                     if force_permissive not in [True, False]:
1373                         raise ValueError(_('{0}_params should have a boolean'
1374                                            ' not a {0} for second argument'
1375                                            ).format(type_, type(
1376                                                force_permissive)))