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