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