basetypes in another module
[tiramisu.git] / option.py
1 # -*- coding: utf-8 -*-
2 "option types and option description for the configuration management"
3 # Copyright (C) 2012 Team tiramisu (see README 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 gus of pypy: pypy: http://codespeak.net/svn/pypy/dist/pypy/config/
21 # the whole pypy projet is under MIT licence
22 # ____________________________________________________________
23 from autolib import special_owners
24 from basetype import HiddenBaseType, DisabledBaseType, ModeBaseType, modes
25 from error import (ConfigError, ConflictConfigError, NotFoundError, 
26                                                                   RequiresError)
27 available_actions = ['hide', 'show', 'enable', 'disable'] 
28 reverse_actions = {'hide': 'show', 'show': 'hide', 
29                    'disable':'enable', 'enable': 'disable'}
30 # ____________________________________________________________
31 # OptionDescription authorized group_type values
32 group_types = ['default', 'family', 'group', 'master']
33 # ____________________________________________________________
34 class Option(HiddenBaseType, DisabledBaseType, ModeBaseType):
35     #reminder: an Option object is **not** a container for the value
36     _frozen = False
37     def __init__(self, name, doc, default=None, requires=None, 
38                     mandatory=False, multi=False, callback=None, mode='normal'):
39         self._name = name
40         self.doc = doc
41         self._requires = requires
42         self._mandatory = mandatory
43         self.multi = multi
44         self.callback = callback
45         if mode not in modes:
46             raise ConfigError("mode {0} not available".format(mode))
47         self.mode = mode
48         if default != None:
49             if not self.validate(default):
50                 raise ConfigError("invalid default value {0} " 
51                                          "for option {1}".format(default, name))
52         self.default = default
53         
54     def validate(self, value):
55         raise NotImplementedError('abstract base class')
56
57     def getdefault(self):
58         return self.default
59
60     def getdoc(self):
61         return self.doc
62
63     def getcallback(self):
64         return self.callback
65
66     def setowner(self, config, who):
67         name = self._name
68         if self._frozen:
69             raise TypeError("trying to change a frozen option's owner: %s" % name)
70         if who in special_owners:
71             if self.callback == None:
72                 raise SpecialOwnersError("no callback specified for" 
73                                                       "option {0}".format(name))
74         config._cfgimpl_value_owners[name] = who
75
76     def setoption(self, config, value, who):
77         name = self._name
78         if self._frozen:
79             raise TypeError('trying to change a frozen option object: %s' % name)
80         # we want the possibility to reset everything
81         if who == "default" and value is None:
82             self.default = None
83             return
84         if not self.validate(value):
85             raise ConfigError('invalid value %s for option %s' % (value, name))
86         if who == "default":
87             # changes the default value (and therefore resets the previous value)
88             self.default = value
89         apply_requires(self, config)
90         # FIXME put the validation for the multi somewhere else
91 #            # it is a multi **and** it has requires
92 #            if self.multi == True:
93 #                if type(value) != list:
94 #                    raise TypeError("value {0} must be a list".format(value))
95 #                if self._requires is not None:
96 #                    for reqname in self._requires:
97 #                        # FIXME : verify that the slaves are all multi
98 #                        #option = getattr(config._cfgimpl_descr, reqname)
99 #    #                    if not option.multi == True:
100 #    #                        raise ConflictConfigError("an option with requires "
101 #    #                         "has to be a list type : {0}".format(name)) 
102 #                        if len(config._cfgimpl_values[reqname]) != len(value):
103 #                            raise ConflictConfigError("an option with requires "
104 #                             "has not the same length of the others " 
105 #                             "in the group : {0}".format(reqname)) 
106         config._cfgimpl_previous_values[name] = config._cfgimpl_values[name] 
107         config._cfgimpl_values[name] = value
108
109     def getkey(self, value):
110         return value
111
112     def freeze(self):
113         self._frozen = True
114         return True
115
116     def unfreeze(self):
117         self._frozen = False
118     # ____________________________________________________________
119     def is_multi(self):
120         return self.multi
121
122     def is_mandatory(self):
123         return self._mandatory
124         
125 class ChoiceOption(Option):
126     opt_type = 'string'
127     
128     def __init__(self, name, doc, values, default=None, requires=None,
129                                                  multi=False, mandatory=False):
130         self.values = values
131         super(ChoiceOption, self).__init__(name, doc, default=default, 
132                            requires=requires, multi=multi, mandatory=mandatory)
133
134     def setoption(self, config, value, who):
135         name = self._name
136         super(ChoiceOption, self).setoption(config, value, who)
137
138     def validate(self, value):
139         if self.multi == False:
140             return value is None or value in self.values
141         else:
142             for val in value:
143                 if not (val is None or val in self.values):
144                     return False
145             return True
146
147 class BoolOption(Option):
148     opt_type = 'bool'
149     
150     def __init__(self, *args, **kwargs):
151         super(BoolOption, self).__init__(*args, **kwargs)
152 #    def __init__(self, name, doc, default=None, requires=None,
153 #                                  validator=None, multi=False, mandatory=False):
154 #        super(BoolOption, self).__init__(name, doc, default=default, 
155 #                            requires=requires, multi=multi, mandatory=mandatory)
156         #self._validator = validator
157
158     def validate(self, value):
159         if self.multi == False:
160             return isinstance(value, bool)
161         else:
162             try:
163                 for val in value:
164                     if not isinstance(val, bool):
165                         return False
166             except Exception:
167                 return False
168             return True
169 # FIXME config level validator             
170 #    def setoption(self, config, value, who):
171 #        name = self._name
172 #        if value and self._validator is not None:
173 #            toplevel = config._cfgimpl_get_toplevel()
174 #            self._validator(toplevel)
175 #        super(BoolOption, self).setoption(config, value, who)
176
177 class IntOption(Option):
178     opt_type = 'int'
179     
180     def __init__(self, *args, **kwargs):
181         super(IntOption, self).__init__(*args, **kwargs)
182
183     def validate(self, value):
184         if self.multi == False:
185             try:
186                 int(value)
187             except TypeError:
188                 return False
189             return True
190         else:
191             for val in value:
192                 try:
193                     int(val)
194                 except TypeError:
195                     return False
196             return True
197                             
198     def setoption(self, config, value, who):
199         try:
200             super(IntOption, self).setoption(config, value, who)
201         except TypeError, e:
202             raise ConfigError(*e.args)
203
204 class FloatOption(Option):
205     opt_type = 'float'
206
207     def __init__(self, *args, **kwargs):
208         super(FloatOption, self).__init__(*args, **kwargs)
209
210     def validate(self, value):
211         if self.multi == False:
212             try:
213                 float(value)
214             except TypeError:
215                 return False
216             return True
217         else:
218             for val in value:
219                try:
220                    float(val)
221                except TypeError:
222                    return False
223             return True
224
225     def setoption(self, config, value, who):
226         try:
227             super(FloatOption, self).setoption(config, float(value), who)
228         except TypeError, e:
229             raise ConfigError(*e.args)
230
231 class StrOption(Option):
232     opt_type = 'string'
233     
234     def __init__(self, *args, **kwargs):
235         super(StrOption, self).__init__(*args, **kwargs)
236
237     def validate(self, value):
238         if self.multi == False:
239             return isinstance(value, str)
240         else:
241             for val in value:
242                 if not isinstance(val, str):
243                     return False
244                 else: 
245                     return True
246                                      
247     def setoption(self, config, value, who):
248         try:
249             super(StrOption, self).setoption(config, value, who)
250         except TypeError, e:
251             raise ConfigError(*e.args)
252
253 class SymLinkOption(object): #(HiddenBaseType, DisabledBaseType):
254     opt_type = 'symlink'
255     
256     def __init__(self, name, path):
257         self._name = name
258         self.path = path 
259     
260     def setoption(self, config, value, who):
261         try:
262             setattr(config, self.path, value) # .setoption(self.path, value, who)
263         except TypeError, e:
264             raise ConfigError(*e.args)
265
266 class IPOption(Option):
267     opt_type = 'ip'
268     
269     def __init__(self, *args, **kwargs):
270         super(IPOption, self).__init__(*args, **kwargs)
271
272     def validate(self, value):
273         # by now the validation is nothing but a string, use IPy instead
274         if self.multi == False:
275             return isinstance(value, str)
276         else:
277             for val in value:
278                 if not isinstance(val, str):
279                     return False
280                 else: 
281                     return True
282                                      
283     def setoption(self, config, value, who):
284         try:
285             super(IPOption, self).setoption(config, value, who)
286         except TypeError, e:
287             raise ConfigError(*e.args)
288
289 class NetmaskOption(Option):
290     opt_type = 'netmask'
291     
292     def __init__(self, *args, **kwargs):
293         super(NetmaskOption, self).__init__(*args, **kwargs)
294
295     def validate(self, value):
296         # by now the validation is nothing but a string, use IPy instead
297         if self.multi == False:
298             return isinstance(value, str)
299         else:
300             for val in value:
301                 if not isinstance(val, str):
302                     return False
303                 else: 
304                     return True
305                                      
306     def setoption(self, config, value, who):
307         try:
308             super(NetmaskOption, self).setoption(config, value, who)
309         except TypeError, e:
310             raise ConfigError(*e.args)
311
312 class ArbitraryOption(Option):
313     def __init__(self, name, doc, default=None, defaultfactory=None, 
314                                    requires=None, multi=False, mandatory=False):
315         super(ArbitraryOption, self).__init__(name, doc, requires=requires,
316                                                multi=multi, mandatory=mandatory)
317         self.defaultfactory = defaultfactory
318         if defaultfactory is not None:
319             assert default is None
320
321     def validate(self, value):
322         return True
323
324     def getdefault(self):
325         if self.defaultfactory is not None:
326             return self.defaultfactory()
327         return self.default
328
329 class OptionDescription(HiddenBaseType, DisabledBaseType, ModeBaseType):
330     group_type = 'default'
331     
332     def __init__(self, name, doc, children, requires=None):
333         self._name = name
334         self.doc = doc
335         self._children = children
336         self._requires = requires
337         self._build()
338     
339     def getdoc(self):
340         return self.doc
341
342     def _build(self):
343         for child in self._children:
344             setattr(self, child._name, child)
345
346     def add_child(self, child):
347         "dynamically adds a configuration option"
348         #Nothing is static. Even the Mona Lisa is falling apart.
349         for ch in self._children:
350             if isinstance(ch, Option):
351                 if child._name == ch._name:
352                     raise ConflictConfigError("existing option : {0}".format(
353                                                                    child._name))
354         self._children.append(child)
355         setattr(self, child._name, child)
356     
357     def update_child(self, child):
358         "modification of an existing option"
359         # XXX : corresponds to the `redefine`, is it usefull 
360         pass
361         
362     def getkey(self, config):
363         return tuple([child.getkey(getattr(config, child._name))
364                       for child in self._children])
365
366     def getpaths(self, include_groups=False, currpath=None):
367         """returns a list of all paths in self, recursively
368            currpath should not be provided (helps with recursion)
369         """
370         if currpath is None:
371             currpath = []
372         paths = []
373         for option in self._children:
374             attr = option._name
375             if attr.startswith('_cfgimpl'):
376                 continue
377             value = getattr(self, attr)
378             if isinstance(value, OptionDescription):
379                 if include_groups:
380                     paths.append('.'.join(currpath + [attr]))
381                 currpath.append(attr)
382                 paths += value.getpaths(include_groups=include_groups,
383                                         currpath=currpath)
384                 currpath.pop()
385             else:
386                 paths.append('.'.join(currpath + [attr]))
387         return paths
388     # ____________________________________________________________
389
390     def set_group_type(self, group_type):
391         if group_type in group_types:
392             self.group_type = group_type
393         else:
394             raise ConfigError('not allowed value for group_type : {0}'.format(
395                               group_type))
396     
397     def get_group_type(self):
398         return self.group_type
399     # ____________________________________________________________
400     def hide(self):
401         super(OptionDescription, self).hide()
402         # FIXME : AND THE SUBCHILDREN ? 
403         for child in self._children:
404             if isinstance(child, OptionDescription):
405                 child.hide()
406     
407     def show(self):
408         # FIXME : AND THE SUBCHILDREN ?? 
409         super(OptionDescription, self).show()
410         for child in self._children:
411             if isinstance(child, OptionDescription):
412                 child.show()
413     # ____________________________________________________________
414     def disable(self):
415         super(OptionDescription, self).disable()
416         # FIXME : AND THE SUBCHILDREN ? 
417         for child in self._children:
418             if isinstance(child, OptionDescription):
419                 child.disable()
420     
421     def enable(self):
422         # FIXME : AND THE SUBCHILDREN ? 
423         super(OptionDescription, self).enable()
424         for child in self._children:
425             if isinstance(child, OptionDescription):
426                 child.enable()
427 # ____________________________________________________________
428 def apply_requires(opt, config):
429     if hasattr(opt, '_requires'):
430         if opt._requires is not None:
431             # malformed requirements
432             rootconfig = config._cfgimpl_get_toplevel()
433             for req in opt._requires:
434                 if not type(req) == tuple and len(req) in (3, 4):
435                     raise RequiresError("malformed requirements for option:"
436                                                    " {0}".format(opt._name))
437             # all actions **must** be identical
438             actions = [req[2] for req in opt._requires]
439             action = actions[0]
440             for act in actions:
441                 if act != action:
442                     raise RequiresError("malformed requirements for option:"
443                                                    " {0}".format(opt._name))
444             # filters the callbacks
445             matches = False
446             for req in opt._requires:
447                 if len(req) == 3:
448                     name, expected, action = req
449                     inverted = False
450                 if len(req) == 4:
451                     name, expected, action, inverted = req
452                     if inverted == 'inverted':
453                         inverted = True
454                 homeconfig, shortname = \
455                                       rootconfig._cfgimpl_get_home_by_path(name)
456                 # FIXME: doesn't work with 'auto' or 'fill' yet 
457                 # (copy the code from the __getattr__
458                 if shortname in homeconfig._cfgimpl_values:
459                     value = homeconfig._cfgimpl_values[shortname]
460                     if (not inverted and value == expected) or \
461                             (inverted and value != expected):
462                         if action not in available_actions:
463                             raise RequiresError("malformed requirements"
464                                            " for option: {0}".format(opt._name))
465                         getattr(opt, action)() #.hide() or show() or...
466                         matches = True
467                 else: # option doesn't exist ! should not happen...
468                     raise NotFoundError("required option not found: "
469                                                              "{0}".format(name))
470             # no callback has been triggered, then just reverse the action
471             if not matches:
472                 getattr(opt, reverse_actions[action])()
473