]> git.llucax.com Git - software/pymin.git/blob - pymin/services/util.py
Merge jack2@192.168.7.8:src/pymin
[software/pymin.git] / pymin / services / util.py
1 # vim: set encoding=utf-8 et sw=4 sts=4 :
2
3 import subprocess
4 from mako.template import Template
5 from mako.runtime import Context
6 from os import path
7 try:
8     import cPickle as pickle
9 except ImportError:
10     import pickle
11
12 from pymin.dispatcher import Handler, handler, HandlerError, \
13                                 CommandNotFoundError
14
15 DEBUG = False
16 #DEBUG = True
17
18 __ALL__ = ('Error', 'ReturnNot0Error', 'ExecutionError', 'ItemError',
19             'ItemAlreadyExistsError', 'ItemNotFoundError', 'ContainerError',
20             'ContainerNotFoundError', 'call', 'get_network_devices',
21             'Persistent', 'Restorable', 'ConfigWriter', 'ServiceHandler',
22             'RestartHandler', 'ReloadHandler', 'InitdHandler', 'SubHandler',
23             'DictSubHandler', 'ListSubHandler', 'ComposedSubHandler',
24             'ListComposedSubHandler', 'DictComposedSubHandler')
25
26 class Error(HandlerError):
27     r"""
28     Error(message) -> Error instance :: Base ServiceHandler exception class.
29
30     All exceptions raised by the ServiceHandler inherits from this one, so
31     you can easily catch any ServiceHandler exception.
32
33     message - A descriptive error message.
34     """
35     pass
36
37 class ReturnNot0Error(Error):
38     r"""
39     ReturnNot0Error(return_value) -> ReturnNot0Error instance.
40
41     A command didn't returned the expected 0 return value.
42
43     return_value - Return value returned by the command.
44     """
45
46     def __init__(self, return_value):
47         r"Initialize the object. See class documentation for more info."
48         self.return_value = return_value
49
50     def __unicode__(self):
51         return 'The command returned %d' % self.return_value
52
53 class ExecutionError(Error):
54     r"""
55     ExecutionError(command, error) -> ExecutionError instance.
56
57     Error executing a command.
58
59     command - Command that was tried to execute.
60
61     error - Error received when trying to execute the command.
62     """
63
64     def __init__(self, command, error):
65         r"Initialize the object. See class documentation for more info."
66         self.command = command
67         self.error = error
68
69     def __unicode__(self):
70         command = self.command
71         if not isinstance(self.command, basestring):
72             command = ' '.join(command)
73         return "Can't execute command %s: %s" % (command, self.error)
74
75 class ParameterError(Error, KeyError):
76     r"""
77     ParameterError(paramname) -> ParameterError instance
78
79     This is the base exception for all DhcpHandler parameters related errors.
80     """
81
82     def __init__(self, paramname):
83         r"Initialize the object. See class documentation for more info."
84         self.message = 'Parameter error: "%s"' % paramname
85
86 class ParameterNotFoundError(ParameterError):
87     r"""
88     ParameterNotFoundError(paramname) -> ParameterNotFoundError instance
89
90     This exception is raised when trying to operate on a parameter that doesn't
91     exists.
92     """
93
94     def __init__(self, paramname):
95         r"Initialize the object. See class documentation for more info."
96         self.message = 'Parameter not found: "%s"' % paramname
97
98 class ItemError(Error, KeyError):
99     r"""
100     ItemError(key) -> ItemError instance.
101
102     This is the base exception for all item related errors.
103     """
104
105     def __init__(self, key):
106         r"Initialize the object. See class documentation for more info."
107         self.message = u'Item error: "%s"' % key
108
109 class ItemAlreadyExistsError(ItemError):
110     r"""
111     ItemAlreadyExistsError(key) -> ItemAlreadyExistsError instance.
112
113     This exception is raised when trying to add an item that already exists.
114     """
115
116     def __init__(self, key):
117         r"Initialize the object. See class documentation for more info."
118         self.message = u'Item already exists: "%s"' % key
119
120 class ItemNotFoundError(ItemError):
121     r"""
122     ItemNotFoundError(key) -> ItemNotFoundError instance.
123
124     This exception is raised when trying to operate on an item that doesn't
125     exists.
126     """
127
128     def __init__(self, key):
129         r"Initialize the object. See class documentation for more info."
130         self.message = u'Item not found: "%s"' % key
131
132 class ContainerError(Error, KeyError):
133     r"""
134     ContainerError(key) -> ContainerError instance.
135
136     This is the base exception for all container related errors.
137     """
138
139     def __init__(self, key):
140         r"Initialize the object. See class documentation for more info."
141         self.message = u'Container error: "%s"' % key
142
143 class ContainerNotFoundError(ContainerError):
144     r"""
145     ContainerNotFoundError(key) -> ContainerNotFoundError instance.
146
147     This exception is raised when trying to operate on an container that
148     doesn't exists.
149     """
150
151     def __init__(self, key):
152         r"Initialize the object. See class documentation for more info."
153         self.message = u'Container not found: "%s"' % key
154
155
156 def get_network_devices():
157     p = subprocess.Popen(('ip', '-o', 'link'), stdout=subprocess.PIPE,
158                                                     close_fds=True)
159     string = p.stdout.read()
160     p.wait()
161     d = dict()
162     devices = string.splitlines()
163     for dev in devices:
164         mac = ''
165         if dev.find('link/ether') != -1:
166             i = dev.find('link/ether')
167             mac = dev[i+11 : i+11+17]
168             i = dev.find(':',2)
169             name = dev[3: i]
170             d[name] = mac
171         elif dev.find('link/ppp') != -1:
172             i = dev.find('link/ppp')
173             mac =  '00:00:00:00:00:00'
174             i = dev.find(':',2)
175             name = dev[3 : i]
176             d[name] = mac
177     return d
178
179 def call(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
180             stderr=subprocess.PIPE, close_fds=True, universal_newlines=True,
181             **kw):
182     if DEBUG:
183         if not isinstance(command, basestring):
184             command = ' '.join(command)
185         print 'Executing command:', command
186         return
187     try:
188         print 'Executing command:', command
189         r = subprocess.call(command, stdin=stdin, stdout=stdout, stderr=stderr,
190                                 universal_newlines=universal_newlines,
191                                 close_fds=close_fds, **kw)
192     except Exception, e:
193         raise ExecutionError(command, e)
194     if r is not 0:
195         raise ExecutionError(command, ReturnNot0Error(r))
196
197 class Persistent:
198     r"""Persistent([attrs[, dir[, ext]]]) -> Persistent.
199
200     This is a helper class to inherit from to automatically handle data
201     persistence using pickle.
202
203     The variables attributes to persist (attrs), and the pickle directory (dir)
204     and file extension (ext) can be defined by calling the constructor or in a
205     more declarative way as class attributes, like:
206
207     class TestHandler(Persistent):
208         _persistent_attrs = ('some_attr', 'other_attr')
209         _persistent_dir = 'persistent-data'
210         _persistent_ext = '.pickle'
211
212     The default dir is '.' and the default extension is '.pkl'. There are no
213     default variables, and they should be specified as string if a single
214     attribute should be persistent or as a tuple of strings if they are more.
215     The strings should be the attribute names to be persisted. For each
216     attribute a separated pickle file is generated in the pickle directory.
217
218     You can call _dump() and _load() to write and read the data respectively.
219     """
220     # TODO implement it using metaclasses to add the handlers method by demand
221     # (only for specifieds commands).
222
223     _persistent_attrs = ()
224     _persistent_dir = '.'
225     _persistent_ext = '.pkl'
226
227     def __init__(self, attrs=None, dir=None, ext=None):
228         r"Initialize the object, see the class documentation for details."
229         if attrs is not None:
230             self._persistent_attrs = attrs
231         if dir is not None:
232             self._persistent_dir = dir
233         if ext is not None:
234             self._persistent_ext = ext
235
236     def _dump(self):
237         r"_dump() -> None :: Dump all persistent data to pickle files."
238         if isinstance(self._persistent_attrs, basestring):
239             self._persistent_attrs = (self._persistent_attrs,)
240         for attrname in self._persistent_attrs:
241             self._dump_attr(attrname)
242
243     def _load(self):
244         r"_load() -> None :: Load all persistent data from pickle files."
245         if isinstance(self._persistent_attrs, basestring):
246             self._persistent_attrs = (self._persistent_attrs,)
247         for attrname in self._persistent_attrs:
248             self._load_attr(attrname)
249
250     def _dump_attr(self, attrname):
251         r"_dump_attr() -> None :: Dump a specific variable to a pickle file."
252         f = file(self._pickle_filename(attrname), 'wb')
253         pickle.dump(getattr(self, attrname), f, 2)
254         f.close()
255
256     def _load_attr(self, attrname):
257         r"_load_attr() -> object :: Load a specific pickle file."
258         f = file(self._pickle_filename(attrname))
259         setattr(self, attrname, pickle.load(f))
260         f.close()
261
262     def _pickle_filename(self, name):
263         r"_pickle_filename() -> string :: Construct a pickle filename."
264         return path.join(self._persistent_dir, name) + self._persistent_ext
265
266 class Restorable(Persistent):
267     r"""Restorable([defaults]) -> Restorable.
268
269     This is a helper class to inherit from that provides a nice _restore()
270     method to restore the persistent data if any, or load some nice defaults
271     if not.
272
273     The defaults can be defined by calling the constructor or in a more
274     declarative way as class attributes, like:
275
276     class TestHandler(Restorable):
277         _persistent_attrs = ('some_attr', 'other_attr')
278         _restorable_defaults = dict(
279                 some_attr = 'some_default',
280                 other_attr = 'other_default')
281
282     The defaults is a dictionary, very coupled with the _persistent_attrs
283     attribute inherited from Persistent. The defaults keys should be the
284     values from _persistent_attrs, and the values the default values.
285
286     The _restore() method returns True if the data was restored successfully
287     or False if the defaults were loaded (in case you want to take further
288     actions). If a _write_config method if found, it's executed when a restore
289     fails too.
290     """
291     # TODO implement it using metaclasses to add the handlers method by demand
292     # (only for specifieds commands).
293
294     _restorable_defaults = dict()
295
296     def __init__(self, defaults=None):
297         r"Initialize the object, see the class documentation for details."
298         if defaults is not None:
299             self._restorable_defaults = defaults
300
301     def _restore(self):
302         r"_restore() -> bool :: Restore persistent data or create a default."
303         try:
304             self._load()
305             return True
306         except IOError:
307             for (k, v) in self._restorable_defaults.items():
308                 setattr(self, k, v)
309             # TODO tener en cuenta servicios que hay que levantar y los que no
310             if hasattr(self, 'commit'):
311                 self.commit()
312                 return False
313             self._dump()
314             if hasattr(self, '_write_config'):
315                 self._write_config()
316             return False
317
318 class ConfigWriter:
319     r"""ConfigWriter([initd_name[, initd_dir]]) -> ConfigWriter.
320
321     This is a helper class to inherit from to automatically handle
322     configuration generation. Mako template system is used for configuration
323     files generation.
324
325     The configuration filenames, the generated configuration files directory
326     and the templates directory can be defined by calling the constructor or
327     in a more declarative way as class attributes, like:
328
329     class TestHandler(ConfigWriter):
330         _config_writer_files = ('base.conf', 'custom.conf')
331         _config_writer_cfg_dir = {
332                                     'base.conf': '/etc/service',
333                                     'custom.conf': '/etc/service/conf.d',
334                                  }
335         _config_writer_tpl_dir = 'templates'
336
337     The generated configuration files directory defaults to '.' and the
338     templates directory to 'templates'. _config_writer_files has no default and
339     must be specified in either way. It can be string or a tuple if more than
340     one configuration file must be generated. _config_writer_cfg_dir could be a
341     dict mapping which file should be stored in which directory, or a single
342     string if all the config files should go to the same directory.
343
344     The template filename and the generated configuration filename are both the
345     same (so if you want to generate some /etc/config, you should have some
346     templates/config template). That's why _config_writer_cfg_dir and
347     _config_writer_tpl_dir can't be the same. This is not true for very
348     specific cases where _write_single_config() is used.
349
350     When you write your Handler, you should call _config_build_templates() in
351     you Handler constructor to build the templates.
352
353     To write the configuration files, you must use the _write_config() method.
354     To know what variables to replace in the template, you have to provide a
355     method called _get_config_vars(tamplate_name), which should return a
356     dictionary of variables to pass to the template system to be replaced in
357     the template for the configuration file 'config_file'.
358     """
359     # TODO implement it using metaclasses to add the handlers method by demand
360     # (only for specifieds commands).
361
362     _config_writer_files = ()
363     _config_writer_cfg_dir = '.'
364     _config_writer_tpl_dir = 'templates'
365
366     def __init__(self, files=None, cfg_dir=None, tpl_dir=None):
367         r"Initialize the object, see the class documentation for details."
368         if files is not None:
369             self._config_writer_files = files
370         if cfg_dir is not None:
371             self._config_writer_cfg_dir = cfg_dir
372         if tpl_dir is not None:
373             self._config_writer_tpl_dir = tpl_dir
374         self._config_build_templates()
375
376     def _config_build_templates(self):
377         r"_config_writer_templates() -> None :: Build the template objects."
378         if isinstance(self._config_writer_files, basestring):
379             self._config_writer_files = (self._config_writer_files,)
380         if not hasattr(self, '_config_writer_templates') \
381                                         or not self._config_writer_templates:
382             self._config_writer_templates = dict()
383             for t in self._config_writer_files:
384                 f = path.join(self._config_writer_tpl_dir, t)
385                 self._config_writer_templates[t] = Template(filename=f)
386
387     def _render_config(self, template_name, vars=None):
388         r"""_render_config(template_name[, config_filename[, vars]]).
389
390         Render a single config file using the template 'template_name'. If
391         vars is specified, it's used as the dictionary with the variables
392         to replace in the templates, if not, it looks for a
393         _get_config_vars() method to get it.
394         """
395         if vars is None:
396             if hasattr(self, '_get_config_vars'):
397                 vars = self._get_config_vars(template_name)
398             else:
399                 vars = dict()
400         elif callable(vars):
401             vars = vars(template_name)
402         return self._config_writer_templates[template_name].render(**vars)
403
404     def _get_config_path(self, template_name, config_filename=None):
405         r"Get a complete configuration path."
406         if not config_filename:
407             config_filename = template_name
408         if isinstance(self._config_writer_cfg_dir, basestring):
409             return path.join(self._config_writer_cfg_dir, config_filename)
410         return path.join(self._config_writer_cfg_dir[template_name],
411                             config_filename)
412
413     def _write_single_config(self, template_name, config_filename=None, vars=None):
414         r"""_write_single_config(template_name[, config_filename[, vars]]).
415
416         Write a single config file using the template 'template_name'. If no
417         config_filename is specified, the config filename will be the same as
418         the 'template_name' (but stored in the generated config files
419         directory). If it's specified, the generated config file is stored in
420         the file called 'config_filename' (also in the generated files
421         directory). If vars is specified, it's used as the dictionary with the
422         variables to replace in the templates, if not, it looks for a
423         _get_config_vars() method to get it.
424         """
425         if vars is None:
426             if hasattr(self, '_get_config_vars'):
427                 vars = self._get_config_vars(template_name)
428             else:
429                 vars = dict()
430         elif callable(vars):
431             vars = vars(template_name)
432         f = file(self._get_config_path(template_name, config_filename), 'w')
433         ctx = Context(f, **vars)
434         self._config_writer_templates[template_name].render_context(ctx)
435         f.close()
436
437     def _write_config(self):
438         r"_write_config() -> None :: Generate all the configuration files."
439         for t in self._config_writer_files:
440             self._write_single_config(t)
441
442
443 class ServiceHandler(Handler, Restorable):
444     r"""ServiceHandler([start[, stop[, restart[, reload]]]]) -> ServiceHandler.
445
446     This is a helper class to inherit from to automatically handle services
447     with start, stop, restart, reload actions.
448
449     The actions can be defined by calling the constructor with all the
450     parameters or in a more declarative way as class attributes, like:
451
452     class TestHandler(ServiceHandler):
453         _service_start = ('command', 'start')
454         _service_stop = ('command', 'stop')
455         _service_restart = ('command', 'restart')
456         _service_reload = 'reload-command'
457
458     Commands are executed without using the shell, that's why they are specified
459     as tuples (where the first element is the command and the others are the
460     command arguments). If only a command is needed (without arguments) a single
461     string can be specified.
462
463     All commands must be specified.
464     """
465     # TODO implement it using metaclasses to add the handlers method by demand
466     # (only for specifieds commands).
467
468     def __init__(self, start=None, stop=None, restart=None, reload=None):
469         r"Initialize the object, see the class documentation for details."
470         for (name, action) in dict(start=start, stop=stop, restart=restart,
471                                                     reload=reload).items():
472             if action is not None:
473                 setattr(self, '_service_%s' % name, action)
474         self._persistent_attrs = list(self._persistent_attrs)
475         self._persistent_attrs.append('_service_running')
476         if '_service_running' not in self._restorable_defaults:
477             self._restorable_defaults['_service_running'] = False
478         self._restore()
479         if self._service_running:
480             self._service_running = False
481             self.start()
482
483     @handler(u'Start the service.')
484     def start(self):
485         r"start() -> None :: Start the service."
486         if not self._service_running:
487             if callable(self._service_start):
488                 self._service_start()
489             else:
490                 call(self._service_start)
491             self._service_running = True
492             self._dump_attr('_service_running')
493
494     @handler(u'Stop the service.')
495     def stop(self):
496         r"stop() -> None :: Stop the service."
497         if self._service_running:
498             if callable(self._service_stop):
499                 self._service_stop()
500             else:
501                 call(self._service_stop)
502             self._service_running = False
503             self._dump_attr('_service_running')
504
505     @handler(u'Restart the service.')
506     def restart(self):
507         r"restart() -> None :: Restart the service."
508         if callable(self._service_restart):
509             self._service_restart()
510         else:
511             call(self._service_restart)
512         self._service_running = True
513         self._dump_attr('_service_running')
514
515     @handler(u'Reload the service config (without restarting, if possible).')
516     def reload(self):
517         r"reload() -> None :: Reload the configuration of the service."
518         if self._service_running:
519             if callable(self._service_reload):
520                 self._service_reload()
521             else:
522                 call(self._service_reload)
523
524     @handler(u'Tell if the service is running.')
525     def running(self):
526         r"reload() -> None :: Reload the configuration of the service."
527         if self._service_running:
528             return 1
529         else:
530             return 0
531
532 class RestartHandler(Handler):
533     r"""RestartHandler() -> RestartHandler :: Provides generic restart command.
534
535     This is a helper class to inherit from to automatically add a restart
536     command that first stop the service and then starts it again (using start
537     and stop commands respectively).
538     """
539
540     @handler(u'Restart the service (alias to stop + start).')
541     def restart(self):
542         r"restart() -> None :: Restart the service calling stop() and start()."
543         self.stop()
544         self.start()
545
546 class ReloadHandler(Handler):
547     r"""ReloadHandler() -> ReloadHandler :: Provides generic reload command.
548
549     This is a helper class to inherit from to automatically add a reload
550     command that calls restart.
551     """
552
553     @handler(u'Reload the service config (alias to restart).')
554     def reload(self):
555         r"reload() -> None :: Reload the configuration of the service."
556         if hasattr(self, '_service_running') and self._service_running:
557             self.restart()
558
559 class InitdHandler(ServiceHandler):
560     # TODO update docs, declarative style is depracated
561     r"""InitdHandler([initd_name[, initd_dir]]) -> InitdHandler.
562
563     This is a helper class to inherit from to automatically handle services
564     with start, stop, restart, reload actions using a /etc/init.d like script.
565
566     The name and directory of the script can be defined by calling the
567     constructor or in a more declarative way as class attributes, like:
568
569     class TestHandler(ServiceHandler):
570         _initd_name = 'some-service'
571         _initd_dir = '/usr/local/etc/init.d'
572
573     The default _initd_dir is '/etc/init.d', _initd_name has no default and
574     must be specified in either way.
575
576     Commands are executed without using the shell.
577     """
578     # TODO implement it using metaclasses to add the handlers method by demand
579     # (only for specifieds commands).
580
581     _initd_dir = '/etc/init.d'
582
583     def __init__(self, initd_name=None, initd_dir=None):
584         r"Initialize the object, see the class documentation for details."
585         if initd_name is not None:
586             self._initd_name = initd_name
587         if initd_dir is not None:
588             self._initd_dir = initd_dir
589         actions = dict()
590         for action in ('start', 'stop', 'restart', 'reload'):
591             actions[action] = (path.join(self._initd_dir, self._initd_name),
592                                 action)
593         ServiceHandler.__init__(self, **actions)
594
595 class TransactionalHandler(Handler):
596     r"""Handle command transactions providing a commit and rollback commands.
597
598     This is a helper class to inherit from to automatically handle
599     transactional handlers, which have commit and rollback commands.
600
601     The handler should provide a reload() method (see ServiceHandler and
602     InitdHandler for helper classes to provide this) which will be called
603     when a commit command is issued (if a reload() command is present).
604     The persistent data will be written too (if a _dump() method is provided,
605     see Persistent and Restorable for that), and the configuration files
606     will be generated (if a _write_config method is present, see ConfigWriter).
607     """
608     # TODO implement it using metaclasses to add the handlers method by demand
609     # (only for specifieds commands).
610
611     @handler(u'Commit the changes (reloading the service, if necessary).')
612     def commit(self):
613         r"commit() -> None :: Commit the changes and reload the service."
614         if hasattr(self, '_dump'):
615             self._dump()
616         unchanged = False
617         if hasattr(self, '_write_config'):
618             unchanged = self._write_config()
619         if not unchanged and hasattr(self, 'reload'):
620             self.reload()
621
622     @handler(u'Discard all the uncommited changes.')
623     def rollback(self):
624         r"rollback() -> None :: Discard the changes not yet commited."
625         if hasattr(self, '_load'):
626             self._load()
627
628 class ParametersHandler(Handler):
629     r"""ParametersHandler([attr]) -> ParametersHandler.
630
631     This is a helper class to inherit from to automatically handle
632     service parameters, providing set, get, list and show commands.
633
634     The attribute that holds the parameters can be defined by calling the
635     constructor or in a more declarative way as class attributes, like:
636
637     class TestHandler(ServiceHandler):
638         _parameters_attr = 'some_attr'
639
640     The default is 'params' and it should be a dictionary.
641     """
642     # TODO implement it using metaclasses to add the handlers method by demand
643     # (only for specifieds commands).
644
645     _parameters_attr = 'params'
646
647     def __init__(self, attr=None):
648         r"Initialize the object, see the class documentation for details."
649         if attr is not None:
650             self._parameters_attr = attr
651
652     @handler(u'Set a service parameter.')
653     def set(self, param, value):
654         r"set(param, value) -> None :: Set a service parameter."
655         if not param in self.params:
656             raise ParameterNotFoundError(param)
657         self.params[param] = value
658         if hasattr(self, '_update'):
659             self._update = True
660
661     @handler(u'Get a service parameter.')
662     def get(self, param):
663         r"get(param) -> None :: Get a service parameter."
664         if not param in self.params:
665             raise ParameterNotFoundError(param)
666         return self.params[param]
667
668     @handler(u'List all available service parameters.')
669     def list(self):
670         r"list() -> tuple :: List all the parameter names."
671         return self.params.keys()
672
673     @handler(u'Get all service parameters, with their values.')
674     def show(self):
675         r"show() -> (key, value) tuples :: List all the parameters."
676         return self.params.items()
677
678 class SubHandler(Handler):
679     r"""SubHandler(parent) -> SubHandler instance :: Handles subcommands.
680
681     This is a helper class to build sub handlers that needs to reference the
682     parent handler.
683
684     parent - Parent Handler object.
685     """
686
687     def __init__(self, parent):
688         r"Initialize the object, see the class documentation for details."
689         self.parent = parent
690
691 class ContainerSubHandler(SubHandler):
692     r"""ContainerSubHandler(parent) -> ContainerSubHandler instance.
693
694     This is a helper class to implement ListSubHandler and DictSubHandler. You
695     should not use it directly.
696
697     The container attribute to handle and the class of objects that it
698     contains can be defined by calling the constructor or in a more declarative
699     way as class attributes, like:
700
701     class TestHandler(ContainerSubHandler):
702         _cont_subhandler_attr = 'some_cont'
703         _cont_subhandler_class = SomeClass
704
705     This way, the parent's some_cont attribute (self.parent.some_cont)
706     will be managed automatically, providing the commands: add, update,
707     delete, get and show. New items will be instances of SomeClass,
708     which should provide a cmp operator to see if the item is on the
709     container and an update() method, if it should be possible to modify
710     it. If SomeClass has an _add, _update or _delete attribute, it set
711     them to true when the item is added, updated or deleted respectively
712     (in case that it's deleted, it's not removed from the container,
713     but it's not listed either).
714     """
715
716     def __init__(self, parent, attr=None, cls=None):
717         r"Initialize the object, see the class documentation for details."
718         self.parent = parent
719         if attr is not None:
720             self._cont_subhandler_attr = attr
721         if cls is not None:
722             self._cont_subhandler_class = cls
723
724     def _attr(self, attr=None):
725         if attr is None:
726             return getattr(self.parent, self._cont_subhandler_attr)
727         setattr(self.parent, self._cont_subhandler_attr, attr)
728
729     def _vattr(self):
730         if isinstance(self._attr(), dict):
731             return dict([(k, i) for (k, i) in self._attr().items()
732                     if not hasattr(i, '_delete') or not i._delete])
733         return [i for i in self._attr()
734                 if not hasattr(i, '_delete') or not i._delete]
735
736     @handler(u'Add a new item')
737     def add(self, *args, **kwargs):
738         r"add(...) -> None :: Add an item to the list."
739         item = self._cont_subhandler_class(*args, **kwargs)
740         if hasattr(item, '_add'):
741             item._add = True
742         key = item
743         if isinstance(self._attr(), dict):
744             key = item.as_tuple()[0]
745         # do we have the same item? then raise an error
746         if key in self._vattr():
747             raise ItemAlreadyExistsError(item)
748         # do we have the same item, but logically deleted? then update flags
749         if key in self._attr():
750             index = key
751             if not isinstance(self._attr(), dict):
752                 index = self._attr().index(item)
753             if hasattr(item, '_add'):
754                 self._attr()[index]._add = False
755             if hasattr(item, '_delete'):
756                 self._attr()[index]._delete = False
757         else: # it's *really* new
758             if isinstance(self._attr(), dict):
759                 self._attr()[key] = item
760             else:
761                 self._attr().append(item)
762
763     @handler(u'Update an item')
764     def update(self, index, *args, **kwargs):
765         r"update(index, ...) -> None :: Update an item of the container."
766         # TODO make it right with metaclasses, so the method is not created
767         # unless the update() method really exists.
768         # TODO check if the modified item is the same of an existing one
769         if not isinstance(self._attr(), dict):
770             index = int(index) # TODO validation
771         if not hasattr(self._cont_subhandler_class, 'update'):
772             raise CommandNotFoundError(('update',))
773         try:
774             item = self._vattr()[index]
775             item.update(*args, **kwargs)
776             if hasattr(item, '_update'):
777                 item._update = True
778         except LookupError:
779             raise ItemNotFoundError(index)
780
781     @handler(u'Delete an item')
782     def delete(self, index):
783         r"delete(index) -> None :: Delete an item of the container."
784         if not isinstance(self._attr(), dict):
785             index = int(index) # TODO validation
786         try:
787             item = self._vattr()[index]
788             if hasattr(item, '_delete'):
789                 item._delete = True
790             else:
791                 del self._attr()[index]
792             return item
793         except LookupError:
794             raise ItemNotFoundError(index)
795
796     @handler(u'Remove all items (use with care).')
797     def clear(self):
798         r"clear() -> None :: Delete all items of the container."
799         if isinstance(self._attr(), dict):
800             self._attr.clear()
801         else:
802             self._attr(list())
803
804     @handler(u'Get information about an item')
805     def get(self, index):
806         r"get(index) -> item :: List all the information of an item."
807         if not isinstance(self._attr(), dict):
808             index = int(index) # TODO validation
809         try:
810             return self._vattr()[index]
811         except LookupError:
812             raise ItemNotFoundError(index)
813
814     @handler(u'Get information about all items')
815     def show(self):
816         r"show() -> list of items :: List all the complete items information."
817         if isinstance(self._attr(), dict):
818             return self._attr().values()
819         return self._vattr()
820
821 class ListSubHandler(ContainerSubHandler):
822     r"""ListSubHandler(parent) -> ListSubHandler instance.
823
824     ContainerSubHandler holding lists. See ComposedSubHandler documentation
825     for details.
826     """
827
828     @handler(u'Get how many items are in the list')
829     def len(self):
830         r"len() -> int :: Get how many items are in the list."
831         return len(self._vattr())
832
833 class DictSubHandler(ContainerSubHandler):
834     r"""DictSubHandler(parent) -> DictSubHandler instance.
835
836     ContainerSubHandler holding dicts. See ComposedSubHandler documentation
837     for details.
838     """
839
840     @handler(u'List all the items by key')
841     def list(self):
842         r"list() -> tuple :: List all the item keys."
843         return self._attr().keys()
844
845 class ComposedSubHandler(SubHandler):
846     r"""ComposedSubHandler(parent) -> ComposedSubHandler instance.
847
848     This is a helper class to implement ListComposedSubHandler and
849     DictComposedSubHandler. You should not use it directly.
850
851     This class is usefull when you have a parent that has a dict (cont)
852     that stores some object that has an attribute (attr) with a list or
853     a dict of objects of some class. In that case, this class provides
854     automated commands to add, update, delete, get and show that objects.
855     This commands takes the cont (key of the dict for the object holding
856     the attr), and an index for access the object itself (in the attr
857     list/dict).
858
859     The container object (cont) that holds a containers, the attribute of
860     that object that is the container itself, and the class of the objects
861     that it contains can be defined by calling the constructor or in a
862     more declarative way as class attributes, like:
863
864     class TestHandler(ComposedSubHandler):
865         _comp_subhandler_cont = 'some_cont'
866         _comp_subhandler_attr = 'some_attr'
867         _comp_subhandler_class = SomeClass
868
869     This way, the parent's some_cont attribute (self.parent.some_cont)
870     will be managed automatically, providing the commands: add, update,
871     delete, get and show for manipulating a particular instance that holds
872     of SomeClass. For example, updating an item at the index 5 is the same
873     (simplified) as doing parent.some_cont[cont][5].update().
874     SomeClass should provide a cmp operator to see if the item is on the
875     container and an update() method, if it should be possible to modify
876     it. If SomeClass has an _add, _update or _delete attribute, it set
877     them to true when the item is added, updated or deleted respectively
878     (in case that it's deleted, it's not removed from the container,
879     but it's not listed either). If the container objects
880     (parent.some_cont[cont]) has an _update attribute, it's set to True
881     when any add, update or delete command is executed.
882     """
883
884     def __init__(self, parent, cont=None, attr=None, cls=None):
885         r"Initialize the object, see the class documentation for details."
886         self.parent = parent
887         if cont is not None:
888             self._comp_subhandler_cont = cont
889         if attr is not None:
890             self._comp_subhandler_attr = attr
891         if cls is not None:
892             self._comp_subhandler_class = cls
893
894     def _cont(self):
895         return getattr(self.parent, self._comp_subhandler_cont)
896
897     def _attr(self, cont, attr=None):
898         if attr is None:
899             return getattr(self._cont()[cont], self._comp_subhandler_attr)
900         setattr(self._cont()[cont], self._comp_subhandler_attr, attr)
901
902     def _vattr(self, cont):
903         if isinstance(self._attr(cont), dict):
904             return dict([(k, i) for (k, i) in self._attr(cont).items()
905                     if not hasattr(i, '_delete') or not i._delete])
906         return [i for i in self._attr(cont)
907                 if not hasattr(i, '_delete') or not i._delete]
908
909     @handler(u'Add a new item')
910     def add(self, cont, *args, **kwargs):
911         r"add(cont, ...) -> None :: Add an item to the list."
912         if not cont in self._cont():
913             raise ContainerNotFoundError(cont)
914         item = self._comp_subhandler_class(*args, **kwargs)
915         if hasattr(item, '_add'):
916             item._add = True
917         key = item
918         if isinstance(self._attr(cont), dict):
919             key = item.as_tuple()[0]
920         # do we have the same item? then raise an error
921         if key in self._vattr(cont):
922             raise ItemAlreadyExistsError(item)
923         # do we have the same item, but logically deleted? then update flags
924         if key in self._attr(cont):
925             index = key
926             if not isinstance(self._attr(cont), dict):
927                 index = self._attr(cont).index(item)
928             if hasattr(item, '_add'):
929                 self._attr(cont)[index]._add = False
930             if hasattr(item, '_delete'):
931                 self._attr(cont)[index]._delete = False
932         else: # it's *really* new
933             if isinstance(self._attr(cont), dict):
934                 self._attr(cont)[key] = item
935             else:
936                 self._attr(cont).append(item)
937         if hasattr(self._cont()[cont], '_update'):
938             self._cont()[cont]._update = True
939
940     @handler(u'Update an item')
941     def update(self, cont, index, *args, **kwargs):
942         r"update(cont, index, ...) -> None :: Update an item of the container."
943         # TODO make it right with metaclasses, so the method is not created
944         # unless the update() method really exists.
945         # TODO check if the modified item is the same of an existing one
946         if not cont in self._cont():
947             raise ContainerNotFoundError(cont)
948         if not isinstance(self._attr(cont), dict):
949             index = int(index) # TODO validation
950         if not hasattr(self._comp_subhandler_class, 'update'):
951             raise CommandNotFoundError(('update',))
952         try:
953             item = self._vattr(cont)[index]
954             item.update(*args, **kwargs)
955             if hasattr(item, '_update'):
956                 item._update = True
957             if hasattr(self._cont()[cont], '_update'):
958                 self._cont()[cont]._update = True
959         except LookupError:
960             raise ItemNotFoundError(index)
961
962     @handler(u'Delete an item')
963     def delete(self, cont, index):
964         r"delete(cont, index) -> None :: Delete an item of the container."
965         if not cont in self._cont():
966             raise ContainerNotFoundError(cont)
967         if not isinstance(self._attr(cont), dict):
968             index = int(index) # TODO validation
969         try:
970             item = self._vattr(cont)[index]
971             if hasattr(item, '_delete'):
972                 item._delete = True
973             else:
974                 del self._attr(cont)[index]
975             if hasattr(self._cont()[cont], '_update'):
976                 self._cont()[cont]._update = True
977             return item
978         except LookupError:
979             raise ItemNotFoundError(index)
980
981     @handler(u'Remove all items (use with care).')
982     def clear(self, cont):
983         r"clear(cont) -> None :: Delete all items of the container."
984         if not cont in self._cont():
985             raise ContainerNotFoundError(cont)
986         if isinstance(self._attr(cont), dict):
987             self._attr(cont).clear()
988         else:
989             self._attr(cont, list())
990
991     @handler(u'Get information about an item')
992     def get(self, cont, index):
993         r"get(cont, index) -> item :: List all the information of an item."
994         if not cont in self._cont():
995             raise ContainerNotFoundError(cont)
996         if not isinstance(self._attr(cont), dict):
997             index = int(index) # TODO validation
998         try:
999             return self._vattr(cont)[index]
1000         except LookupError:
1001             raise ItemNotFoundError(index)
1002
1003     @handler(u'Get information about all items')
1004     def show(self, cont):
1005         r"show(cont) -> list of items :: List all the complete items information."
1006         if not cont in self._cont():
1007             raise ContainerNotFoundError(cont)
1008         if isinstance(self._attr(cont), dict):
1009             return self._attr(cont).values()
1010         return self._vattr(cont)
1011
1012 class ListComposedSubHandler(ComposedSubHandler):
1013     r"""ListComposedSubHandler(parent) -> ListComposedSubHandler instance.
1014
1015     ComposedSubHandler holding lists. See ComposedSubHandler documentation
1016     for details.
1017     """
1018
1019     @handler(u'Get how many items are in the list')
1020     def len(self, cont):
1021         r"len(cont) -> int :: Get how many items are in the list."
1022         if not cont in self._cont():
1023             raise ContainerNotFoundError(cont)
1024         return len(self._vattr(cont))
1025
1026 class DictComposedSubHandler(ComposedSubHandler):
1027     r"""DictComposedSubHandler(parent) -> DictComposedSubHandler instance.
1028
1029     ComposedSubHandler holding dicts. See ComposedSubHandler documentation
1030     for details.
1031     """
1032
1033     @handler(u'List all the items by key')
1034     def list(self, cont):
1035         r"list(cont) -> tuple :: List all the item keys."
1036         if not cont in self._cont():
1037             raise ContainerNotFoundError(cont)
1038         return self._attr(cont).keys()
1039
1040
1041 if __name__ == '__main__':
1042
1043     # Execution tests
1044     class STestHandler1(ServiceHandler):
1045         _service_start = ('service', 'start')
1046         _service_stop = ('service', 'stop')
1047         _service_restart = ('ls', '/')
1048         _service_reload = ('cp', '/la')
1049     class STestHandler2(ServiceHandler):
1050         def __init__(self):
1051             ServiceHandler.__init__(self, 'cmd-start', 'cmd-stop',
1052                                         'cmd-restart', 'cmd-reload')
1053     class ITestHandler1(InitdHandler):
1054         _initd_name = 'test1'
1055     class ITestHandler2(InitdHandler):
1056         def __init__(self):
1057             InitdHandler.__init__(self, 'test2', '/usr/local/etc/init.d')
1058     handlers = [
1059         STestHandler1(),
1060         STestHandler2(),
1061         ITestHandler1(),
1062         ITestHandler2(),
1063     ]
1064     for h in handlers:
1065         print h.__class__.__name__
1066         try:
1067             h.start()
1068         except ExecutionError, e:
1069             print e
1070         try:
1071             h.stop()
1072         except ExecutionError, e:
1073             print e
1074         try:
1075             h.restart()
1076         except ExecutionError, e:
1077             print e
1078         try:
1079             h.reload()
1080         except ExecutionError, e:
1081             print e
1082         print
1083
1084     # Persistent test
1085     print 'PTestHandler'
1086     class PTestHandler(Persistent):
1087         _persistent_attrs = 'vars'
1088         def __init__(self):
1089             self.vars = dict(a=1, b=2)
1090     h = PTestHandler()
1091     print h.vars
1092     h._dump()
1093     h.vars['x'] = 100
1094     print h.vars
1095     h._load()
1096     print h.vars
1097     h.vars['x'] = 100
1098     h._dump()
1099     print h.vars
1100     del h.vars['x']
1101     print h.vars
1102     h._load()
1103     print h.vars
1104     print
1105
1106     # Restorable test
1107     print 'RTestHandler'
1108     class RTestHandler(Restorable):
1109         _persistent_attrs = 'vars'
1110         _restorable_defaults = dict(vars=dict(a=1, b=2))
1111         def __init__(self):
1112             self._restore()
1113     h = RTestHandler()
1114     print h.vars
1115     h.vars['x'] = 100
1116     h._dump()
1117     h = RTestHandler()
1118     print h.vars
1119     print
1120
1121     # ConfigWriter test
1122     print 'CTestHandler'
1123     import os
1124     os.mkdir('templates')
1125     f = file('templates/config', 'w')
1126     f.write('Hello, ${name}! You are ${what}.')
1127     f.close()
1128     print 'template:'
1129     print file('templates/config').read()
1130     class CTestHandler(ConfigWriter):
1131         _config_writer_files = 'config'
1132         def __init__(self):
1133             self._config_build_templates()
1134         def _get_config_vars(self, config_file):
1135             return dict(name='you', what='a parrot')
1136     h = CTestHandler()
1137     h._write_config()
1138     print 'config:'
1139     print file('config').read()
1140     os.unlink('config')
1141     os.unlink('templates/config')
1142     os.rmdir('templates')
1143     print
1144