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