add option name's validation and rename Option method with objimpl_
[tiramisu.git] / tiramisu / option.py
1 # -*- coding: utf-8 -*-
2 "option types and option description for the configuration management"
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 from copy import copy
25 from types import FunctionType
26 from IPy import IP
27
28 from tiramisu.error import ConflictConfigError
29 from tiramisu.setting import groups, multitypes
30 from tiramisu.i18n import _
31
32 name_regexp = re.compile(r'^\d+')
33 forbidden_names = ['iter_all', 'iter_group', 'find', 'find_fisrt',
34                    'make_dict', 'unwrap_from_path']
35
36
37 def valid_name(name):
38     try:
39         name = str(name)
40     except:
41         return False
42     if re.match(name_regexp, name) is None and not name.startswith('_') \
43             and name not in forbidden_names \
44             and not name.startswith('objimpl_') and \
45             not name.startswith('cfgimpl_'):
46         return True
47     else:
48         return False
49 #____________________________________________________________
50 #
51
52
53 class BaseInformation(object):
54     __slots__ = ('_informations')
55
56     def objimpl_set_information(self, key, value):
57         """updates the information's attribute
58         (wich is a dictionnary)
59
60         :param key: information's key (ex: "help", "doc"
61         :param value: information's value (ex: "the help string")
62         """
63         self._informations[key] = value
64
65     def objimpl_get_information(self, key, default=None):
66         """retrieves one information's item
67
68         :param key: the item string (ex: "help")
69         """
70         if key in self._informations:
71             return self._informations[key]
72         elif default is not None:
73             return default
74         else:
75             raise ValueError(_("Information's item not found: {0}").format(key))
76
77
78 class Option(BaseInformation):
79     """
80     Abstract base class for configuration option's.
81
82     Reminder: an Option object is **not** a container for the value
83     """
84     __slots__ = ('_name', '_requires', '_multi', '_validator', '_default_multi',
85                  '_default', '_properties', '_callback', '_multitype',
86                  '_master_slaves', '_consistencies', '_empty')
87     _empty = ''
88
89     def __init__(self, name, doc, default=None, default_multi=None,
90                  requires=None, multi=False, callback=None,
91                  callback_params=None, validator=None, validator_args=None,
92                  properties=None):
93         """
94         :param name: the option's name
95         :param doc: the option's description
96         :param default: specifies the default value of the option,
97                         for a multi : ['bla', 'bla', 'bla']
98         :param default_multi: 'bla' (used in case of a reset to default only at
99                         a given index)
100         :param requires: is a list of names of options located anywhere
101                          in the configuration.
102         :param multi: if true, the option's value is a list
103         :param callback: the name of a function. If set, the function's output
104                          is responsible of the option's value
105         :param callback_params: the callback's parameter
106         :param validator: the name of a function wich stands for a custom
107                           validation of the value
108         :param validator_args: the validator's parameters
109         """
110         if not valid_name(name):
111             raise ValueError(_("invalid name: {0} for option").format(name))
112         self._name = name
113         self._informations = {}
114         self.objimpl_set_information('doc', doc)
115         validate_requires_arg(requires, self._name)
116         self._requires = requires
117         self._multi = multi
118         self._consistencies = None
119         if validator is not None:
120             if type(validator) != FunctionType:
121                 raise TypeError(_("validator must be a function"))
122             if validator_args is None:
123                 validator_args = {}
124             self._validator = (validator, validator_args)
125         else:
126             self._validator = None
127         if not self._multi and default_multi is not None:
128             raise ValueError(_("a default_multi is set whereas multi is False"
129                              " in option: {0}").format(name))
130         if default_multi is not None and not self._validate(default_multi):
131             raise ValueError(_("invalid default_multi value {0} "
132                              "for option {1}").format(str(default_multi), name))
133         if callback is not None and (default is not None or default_multi is not None):
134             raise ValueError(_("defaut values not allowed if option: {0} "
135                              "is calculated").format(name))
136         if callback is None and callback_params is not None:
137             raise ValueError(_("params defined for a callback function but "
138                              "no callback defined yet for option {0}").format(name))
139         if callback is not None:
140             if not isinstance(callback, str):
141                 raise ValueError('callback must be a string')
142             if callback_params is not None and \
143                     not isinstance(callback_params, dict):
144                 raise ValueError('callback_params must be a dict')
145             self._callback = (callback, callback_params)
146         else:
147             self._callback = None
148         if self._multi:
149             if default is None:
150                 default = []
151             #if not isinstance(default, list):
152             #    raise ValidateError("invalid default value {0} "
153             #                        "for option {1} : not list type"
154             #                        "".format(str(default), name))
155             if not self.objimpl_validate(default):
156                 raise ValueError(_("invalid default value {0} "
157                                  "for option {1}"
158                                  "").format(str(default), name))
159             self._multitype = multitypes.default
160             self._default_multi = default_multi
161         else:
162             if default is not None and not self.objimpl_validate(default):
163                 raise ValueError(_("invalid default value {0} "
164                                  "for option {1}").format(str(default), name))
165         self._default = default
166         if properties is None:
167             properties = ()
168         if not isinstance(properties, tuple):
169             raise TypeError(_('invalid properties type {0} for {1},'
170                             ' must be a tuple').format(type(properties), self._name))
171         self._properties = properties  # 'hidden', 'disabled'...
172
173     def objimpl_validate(self, value, context=None, validate=True):
174         """
175         :param value: the option's value
176         :param validate: if true enables ``self._validator`` validation
177         """
178         # generic calculation
179         if context is not None:
180             cons = context.cfgimpl_get_description()
181         else:
182             cons = None
183         if not self._multi:
184             # None allows the reset of the value
185             if value is not None:
186                 # customizing the validator
187                 if validate and self._validator is not None and \
188                         not self._validator[0](value, **self._validator[1]):
189                     return False
190                 if not self._validate(value):
191                     return False
192                 if cons is not None:
193                     return cons._valid_consistency(self, value, context, None)
194         else:
195             if not isinstance(value, list):
196                 raise ValueError(_("invalid value {0} "
197                                    "for option {1} which must be a list"
198                                    "").format(value, self._name))
199             for index in range(0, len(value)):
200                 val = value[index]
201                 # None allows the reset of the value
202                 if val is not None:
203                     # customizing the validator
204                     if validate and self._validator is not None and \
205                             not self._validator[0](val, **self._validator[1]):
206                         return False
207                     if not self._validate(val):
208                         return False
209                     if cons is not None and not cons._valid_consistency(self, val, context, index):
210                         return False
211         return True
212
213     def objimpl_getdefault(self, default_multi=False):
214         "accessing the default value"
215         if not default_multi or not self.objimpl_is_multi():
216             return self._default
217         else:
218             return self.getdefault_multi()
219
220     def objimpl_getdefault_multi(self):
221         "accessing the default value for a multi"
222         return self._default_multi
223
224     def objimpl_get_multitype(self):
225         return self._multitype
226
227     def objimpl_get_master_slaves(self):
228         return self._master_slaves
229
230     def objimpl_is_empty_by_default(self):
231         "no default value has been set yet"
232         if ((not self.objimpl_is_multi() and self._default is None) or
233                 (self.objimpl_is_multi() and (self._default == [] or None in self._default))):
234             return True
235         return False
236
237     def objimpl_getdoc(self):
238         "accesses the Option's doc"
239         return self.objimpl_get_information('doc')
240
241     def objimpl_has_callback(self):
242         "to know if a callback has been defined or not"
243         if self._callback is None:
244             return False
245         else:
246             return True
247
248     def objimpl_getkey(self, value):
249         return value
250
251     def objimpl_is_multi(self):
252         return self._multi
253
254     def objimpl_add_consistency(self, func, opts):
255         pass
256         if self._consistencies is None:
257             self._consistencies = []
258         if self not in opts:
259             opts = list(opts)
260             opts.append(self)
261             opts = tuple(opts)
262         self._consistencies.append(('_cons_{}'.format(func), opts))
263
264     def _cons_not_equal(self, opt, value, context, index, opts):
265         values = [value]
266         descr = context.cfgimpl_get_description()
267         for opt_ in opts:
268             if opt_ is not opt:
269                 path = descr.objimpl_get_path_by_opt(opt_)
270                 val = context._getattr(path, validate=False)
271                 if val is not None:
272                     if val in values:
273                         return False
274                     values.append(val)
275         return True
276
277     def _cons_lower(self, value):
278         try:
279             return value.islower()
280         except AttributeError:
281             #no "islower" attribute
282             return False
283
284
285 class ChoiceOption(Option):
286     __slots__ = ('_values', '_open_values', '_opt_type')
287     _opt_type = 'string'
288
289     def __init__(self, name, doc, values, default=None, default_multi=None,
290                  requires=None, multi=False, callback=None,
291                  callback_params=None, open_values=False, validator=None,
292                  validator_args=None, properties=()):
293         if not isinstance(values, tuple):
294             raise TypeError(_('values must be a tuple for {0}').format(name))
295         self._values = values
296         if open_values not in (True, False):
297             raise TypeError(_('open_values must be a boolean for '
298                             '{0}').format(name))
299         self._open_values = open_values
300         super(ChoiceOption, self).__init__(name, doc, default=default,
301                                            default_multi=default_multi,
302                                            callback=callback,
303                                            callback_params=callback_params,
304                                            requires=requires,
305                                            multi=multi,
306                                            validator=validator,
307                                            validator_args=validator_args,
308                                            properties=properties)
309
310     def _validate(self, value):
311         if not self._open_values:
312             return value is None or value in self._values
313         else:
314             return True
315
316
317 class BoolOption(Option):
318     __slots__ = ('_opt_type')
319     _opt_type = 'bool'
320
321     def _validate(self, value):
322         return isinstance(value, bool)
323
324
325 class IntOption(Option):
326     __slots__ = ('_opt_type')
327     _opt_type = 'int'
328
329     def _validate(self, value):
330         return isinstance(value, int)
331
332
333 class FloatOption(Option):
334     __slots__ = ('_opt_type')
335     _opt_type = 'float'
336
337     def _validate(self, value):
338         return isinstance(value, float)
339
340
341 class StrOption(Option):
342     __slots__ = ('_opt_type')
343     _opt_type = 'string'
344
345     def _validate(self, value):
346         return isinstance(value, str)
347
348
349 class UnicodeOption(Option):
350     __slots__ = ('_opt_type')
351     _opt_type = 'unicode'
352     _empty = u''
353
354     def _validate(self, value):
355         return isinstance(value, unicode)
356
357
358 class SymLinkOption(object):
359     __slots__ = ('_name', '_opt', '_consistencies')
360     _opt_type = 'symlink'
361     _consistencies = None
362
363     def __init__(self, name, path, opt):
364         self._name = name
365         self._opt = opt
366
367     def _setoption(self, context, value):
368         path = context.cfgimpl_get_description().objimpl_get_path_by_opt(self._opt)
369         setattr(context, path, value)
370
371     def __getattr__(self, name):
372         if name in ('_name', '_opt', '_setoption', '_consistencies'):
373             return object.__gettattr__(self, name)
374         else:
375             return getattr(self._opt, name)
376
377
378 class IPOption(Option):
379     __slots__ = ('_opt_type', '_only_private')
380     _opt_type = 'ip'
381
382     def objimpl_set_private(self):
383         self._only_private = True
384
385     def _validate(self, value):
386         try:
387             only_private = self._only_private
388         except AttributeError:
389             only_private = False
390         try:
391             ip = IP('{0}/32'.format(value))
392             if only_private:
393                 return ip.iptype() == 'PRIVATE'
394             return True
395         except ValueError:
396             return False
397
398
399 class NetworkOption(Option):
400     __slots__ = ('_opt_type')
401     _opt_type = 'network'
402
403     def _validate(self, value):
404         try:
405             IP(value)
406             return True
407         except ValueError:
408             return False
409
410
411 class NetmaskOption(Option):
412     __slots__ = ('_opt_type')
413     _opt_type = 'netmask'
414
415     def __init__(self, name, doc, default=None, default_multi=None,
416                  requires=None, multi=False, callback=None,
417                  callback_params=None, validator=None, validator_args=None,
418                  properties=None, opt_ip=None):
419         if opt_ip is not None and not isinstance(opt_ip, IPOption) and \
420                 not isinstance(opt_ip, NetworkOption):
421             raise TypeError(_('opt_ip must be a IPOption not {}').format(type(opt_ip)))
422         super(NetmaskOption, self).__init__(name, doc, default=default,
423                                             default_multi=default_multi,
424                                             callback=callback,
425                                             callback_params=callback_params,
426                                             requires=requires,
427                                             multi=multi,
428                                             validator=validator,
429                                             validator_args=validator_args,
430                                             properties=properties)
431         if opt_ip is None:
432             pass
433         elif isinstance(opt_ip, IPOption):
434             self._consistencies = [('cons_ip_netmask', (self, opt_ip))]
435         elif isinstance(opt_ip, NetworkOption):
436             self._consistencies = [('cons_network_netmask', (self, opt_ip))]
437         else:
438             raise TypeError(_('unknown type for opt_ip'))
439
440     def _validate(self, value):
441         try:
442             IP('0.0.0.0/{}'.format(value))
443             return True
444         except ValueError:
445             return False
446
447     def _cons_network_netmask(self, opt, value, context, index, opts):
448         #opts must be (netmask, network) options
449         return self._cons_netmask(opt, value, context, index, opts, False)
450
451     def _cons_ip_netmask(self, opt, value, context, index, opts):
452         #opts must be (netmask, ip) options
453         return self._cons_netmask(opt, value, context, index, opts, True)
454
455     def __cons_netmask(self, opt, value, context, index, opts, make_net):
456         opt_netmask, opt_ipnetwork = opts
457         descr = context.cfgimpl_get_description()
458         if opt is opt_ipnetwork:
459             val_ipnetwork = value
460             path = descr.objimpl_get_path_by_opt(opt_netmask)
461             val_netmask = context._getattr(path, validate=False)
462             if opt_netmask.objimpl_is_multi():
463                 val_netmask = val_netmask[index]
464             if val_netmask is None:
465                 return True
466         else:
467             val_netmask = value
468             path = descr.objimpl_get_path_by_opt(opt_ipnetwork)
469             val_ipnetwork = getattr(context, path)
470             if opt_ipnetwork.objimpl_is_multi():
471                 val_ipnetwork = val_ipnetwork[index]
472             if val_ipnetwork is None:
473                 return True
474         try:
475             IP('{}/{}'.format(val_ipnetwork, val_netmask, make_net=make_net))
476             return True
477         except ValueError:
478             return False
479
480
481 class DomainnameOption(Option):
482     __slots__ = ('_opt_type', '_type', '_allow_ip')
483     _opt_type = 'domainname'
484     #allow_ip
485
486     def __init__(self, name, doc, default=None, default_multi=None,
487                  requires=None, multi=False, callback=None,
488                  callback_params=None, validator=None, validator_args=None,
489                  properties=None, allow_ip=False, type_='domainname'):
490         #netbios: for MS domain
491         #hostname: to identify the device
492         #domainname:
493         #fqdn: with tld, not supported yet
494         super(NetmaskOption, self).__init__(name, doc, default=default,
495                                             default_multi=default_multi,
496                                             callback=callback,
497                                             callback_params=callback_params,
498                                             requires=requires,
499                                             multi=multi,
500                                             validator=validator,
501                                             validator_args=validator_args,
502                                             properties=properties)
503         if type_ not in ['netbios', 'hostname', 'domainname']:
504             raise ValueError(_('unknown type_ {0} for hostname').format(type_))
505         self._type = type_
506         self._allow_ip = allow_ip
507
508     def _validate(self, value):
509         if self._allow_ip is True:
510             try:
511                 IP('{0}/32'.format(value))
512                 return True
513             except ValueError:
514                 pass
515         if self._type == 'netbios':
516             length = 15
517             extrachar = ''
518         elif self._type == 'hostname':
519             length = 63
520             extrachar = ''
521         elif self._type == 'domainname':
522             length = 255
523             extrachar = '\.'
524         regexp = r'^[a-zA-Z]([a-zA-Z\d-{0}]{{,{1}}})*[a-zA-Z\d]$'.format(
525             extrachar, length - 2)
526         return re.match(regexp, value) is not None
527
528
529 class OptionDescription(BaseInformation):
530     """Config's schema (organisation, group) and container of Options"""
531     __slots__ = ('_name', '_requires', '_cache_paths', '_group_type',
532                  '_properties', '_children', '_consistencies')
533
534     def __init__(self, name, doc, children, requires=None, properties=()):
535         """
536         :param children: is a list of option descriptions (including
537         ``OptionDescription`` instances for nested namespaces).
538         """
539         if not valid_name(name):
540             raise ValueError(_("invalid name: {0} for option descr").format(name))
541         self._name = name
542         self._informations = {}
543         self.objimpl_set_information('doc', doc)
544         child_names = [child._name for child in children]
545         #better performance like this
546         valid_child = copy(child_names)
547         valid_child.sort()
548         old = None
549         for child in valid_child:
550             if child == old:
551                 raise ConflictConfigError(_('duplicate option name: '
552                                           '{0}').format(child))
553             old = child
554         self._children = (tuple(child_names), tuple(children))
555         validate_requires_arg(requires, self._name)
556         self._requires = requires
557         self._cache_paths = None
558         self._consistencies = None
559         if not isinstance(properties, tuple):
560             raise TypeError(_('invalid properties type {0} for {1},'
561                               ' must be a tuple').format(type(properties), self._name))
562         self._properties = properties  # 'hidden', 'disabled'...
563         # the group_type is useful for filtering OptionDescriptions in a config
564         self._group_type = groups.default
565
566     def objimpl_getdoc(self):
567         return self.objimpl_get_information('doc')
568
569     def __getattr__(self, name):
570         try:
571             return self._children[1][self._children[0].index(name)]
572         except ValueError:
573             raise AttributeError(_('unknown Option {} in OptionDescription {}'
574                                  '').format(name, self._name))
575
576     def objimpl_getkey(self, config):
577         return tuple([child.objimpl_getkey(getattr(config, child._name))
578                       for child in self.objimpl_getchildren()])
579
580     def objimpl_getpaths(self, include_groups=False, _currpath=None):
581         """returns a list of all paths in self, recursively
582            _currpath should not be provided (helps with recursion)
583         """
584         if _currpath is None:
585             _currpath = []
586         paths = []
587         for option in self.objimpl_getchildren():
588             attr = option._name
589             if isinstance(option, OptionDescription):
590                 if include_groups:
591                     paths.append('.'.join(_currpath + [attr]))
592                 paths += option.objimpl_getpaths(include_groups=include_groups,
593                                                  _currpath=_currpath + [attr])
594             else:
595                 paths.append('.'.join(_currpath + [attr]))
596         return paths
597
598     def objimpl_getchildren(self):
599         return self._children[1]
600
601     def objimpl_build_cache(self, cache_path=None, cache_option=None, _currpath=None, _consistencies=None):
602         if _currpath is None and self._cache_paths is not None:
603             return
604         if _currpath is None:
605             save = True
606             _currpath = []
607             _consistencies = {}
608         else:
609             save = False
610         if cache_path is None:
611             cache_path = [self._name]
612             cache_option = [self]
613         for option in self.objimpl_getchildren():
614             attr = option._name
615             if attr.startswith('_cfgimpl'):
616                 continue
617             cache_option.append(option)
618             cache_path.append(str('.'.join(_currpath + [attr])))
619             if not isinstance(option, OptionDescription):
620                 if option._consistencies is not None:
621                     for consistency in option._consistencies:
622                         func, opts = consistency
623                         for opt in opts:
624                             _consistencies.setdefault(opt, []).append((func, opts))
625             else:
626                 _currpath.append(attr)
627                 option.objimpl_build_cache(cache_path, cache_option, _currpath, _consistencies)
628                 _currpath.pop()
629         if save:
630             #valid no duplicated option
631             valid_child = copy(cache_option)
632             valid_child.sort()
633             old = None
634             for child in valid_child:
635                 if child == old:
636                     raise ConflictConfigError(_('duplicate option: '
637                                               '{0}').format(child))
638                 old = child
639             self._cache_paths = (tuple(cache_option), tuple(cache_path))
640             self._consistencies = _consistencies
641
642     def obgimpl_get_opt_by_path(self, path):
643         try:
644             return self._cache_paths[0][self._cache_paths[1].index(path)]
645         except ValueError:
646             raise AttributeError(_('no option for path {}').format(path))
647
648     def objimpl_get_path_by_opt(self, opt):
649         try:
650             return self._cache_paths[1][self._cache_paths[0].index(opt)]
651         except ValueError:
652             raise AttributeError(_('no option {} found').format(opt))
653
654     # ____________________________________________________________
655     def objimpl_set_group_type(self, group_type):
656         """sets a given group object to an OptionDescription
657
658         :param group_type: an instance of `GroupType` or `MasterGroupType`
659                               that lives in `setting.groups`
660         """
661         if self._group_type != groups.default:
662             raise TypeError(_('cannot change group_type if already set '
663                             '(old {}, new {})').format(self._group_type, group_type))
664         if isinstance(group_type, groups.GroupType):
665             self._group_type = group_type
666             if isinstance(group_type, groups.MasterGroupType):
667                 #if master (same name has group) is set
668                 identical_master_child_name = False
669                 #for collect all slaves
670                 slaves = []
671                 master = None
672                 for child in self.objimpl_getchildren():
673                     if isinstance(child, OptionDescription):
674                         raise ValueError(_("master group {} shall not have "
675                                          "a subgroup").format(self._name))
676                     if not child.objimpl_is_multi():
677                         raise ValueError(_("not allowed option {0} in group {1}"
678                                          ": this option is not a multi"
679                                          "").format(child._name, self._name))
680                     if child._name == self._name:
681                         identical_master_child_name = True
682                         child._multitype = multitypes.master
683                         master = child
684                     else:
685                         slaves.append(child)
686                 if master is None:
687                     raise ValueError(_('master group with wrong master name for {}'
688                                      '').format(self._name))
689                 master._master_slaves = tuple(slaves)
690                 for child in self.objimpl_getchildren():
691                     if child != master:
692                         child._master_slaves = master
693                         child._multitype = multitypes.slave
694                 if not identical_master_child_name:
695                     raise ValueError(_("the master group: {} has not any "
696                                      "master child").format(self._name))
697         else:
698             raise ValueError(_('not allowed group_type : {0}').format(group_type))
699
700     def objimpl_get_group_type(self):
701         return self._group_type
702
703     def _valid_consistency(self, opt, value, context, index):
704         consistencies = self._consistencies.get(opt)
705         if consistencies is not None:
706             for consistency in consistencies:
707                 func, opts = consistency
708                 ret = getattr(opts[0], func)(opt, value, context, index, opts)
709                 if ret is False:
710                     return False
711         return True
712
713
714 def validate_requires_arg(requires, name):
715     "check malformed requirements"
716     if requires is not None:
717         config_action = {}
718         for req in requires:
719             if not type(req) == tuple:
720                 raise ValueError(_("malformed requirements type for option:"
721                                  " {0}, must be a tuple").format(name))
722             if len(req) == 3:
723                 action = req[2]
724                 inverse = False
725             elif len(req) == 4:
726                 action = req[2]
727                 inverse = req[3]
728             else:
729                 raise ValueError(_("malformed requirements for option: {0}"
730                                  " invalid len").format(name))
731             if action in config_action:
732                 if inverse != config_action[action]:
733                     raise ValueError(_("inconsistency in action types for option: {0}"
734                                      " action: {1}").format(name, action))
735             else:
736                 config_action[action] = inverse