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