]> git.llucax.com Git - software/pymin.git/blob - pymin/services/util.py
Add ComposedSubHandler (and their List and Dict flavors) to services.util.
[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
630     @handler(u'Get a service parameter.')
631     def get(self, param):
632         r"get(param) -> None :: Get a service parameter."
633         if not param in self.params:
634             raise ParameterNotFoundError(param)
635         return self.params[param]
636
637     @handler(u'List all available service parameters.')
638     def list(self):
639         r"list() -> tuple :: List all the parameter names."
640         return self.params.keys()
641
642     @handler(u'Get all service parameters, with their values.')
643     def show(self):
644         r"show() -> (key, value) tuples :: List all the parameters."
645         return self.params.items()
646
647 class SubHandler(Handler):
648     r"""SubHandler(parent) -> SubHandler instance :: Handles subcommands.
649
650     This is a helper class to build sub handlers that needs to reference the
651     parent handler.
652
653     parent - Parent Handler object.
654     """
655
656     def __init__(self, parent):
657         r"Initialize the object, see the class documentation for details."
658         self.parent = parent
659
660 class ContainerSubHandler(SubHandler):
661     r"""ContainerSubHandler(parent) -> ContainerSubHandler instance.
662
663     This is a helper class to implement ListSubHandler and DictSubHandler. You
664     should not use it directly.
665
666     The container attribute to handle and the class of objects that it
667     contains can be defined by calling the constructor or in a more declarative
668     way as class attributes, like:
669
670     class TestHandler(ContainerSubHandler):
671         _cont_subhandler_attr = 'some_cont'
672         _cont_subhandler_class = SomeClass
673
674     This way, the parent's some_cont attribute (self.parent.some_cont)
675     will be managed automatically, providing the commands: add, update,
676     delete, get and show. New items will be instances of SomeClass,
677     which should provide a cmp operator to see if the item is on the
678     container and an update() method, if it should be possible to modify
679     it. If SomeClass has an _add, _update or _delete attribute, it set
680     them to true when the item is added, updated or deleted respectively
681     (in case that it's deleted, it's not removed from the container,
682     but it's not listed either).
683     """
684
685     def __init__(self, parent, attr=None, cls=None):
686         r"Initialize the object, see the class documentation for details."
687         self.parent = parent
688         if attr is not None:
689             self._cont_subhandler_attr = attr
690         if cls is not None:
691             self._cont_subhandler_class = cls
692
693     def _attr(self, attr=None):
694         if attr is None:
695             return getattr(self.parent, self._cont_subhandler_attr)
696         setattr(self.parent, self._cont_subhandler_attr, attr)
697
698     def _vattr(self):
699         if isinstance(self._attr(), dict):
700             return dict([(k, i) for (k, i) in self._attr().items()
701                     if not hasattr(i, '_delete') or not i._delete])
702         return [i for i in self._attr()
703                 if not hasattr(i, '_delete') or not i._delete]
704
705     @handler(u'Add a new item')
706     def add(self, *args, **kwargs):
707         r"add(...) -> None :: Add an item to the list."
708         item = self._cont_subhandler_class(*args, **kwargs)
709         if hasattr(item, '_add'):
710             item._add = True
711         key = item
712         if isinstance(self._attr(), dict):
713             key = item.as_tuple()[0]
714         # do we have the same item? then raise an error
715         if key in self._vattr():
716             raise ItemAlreadyExistsError(item)
717         # do we have the same item, but logically deleted? then update flags
718         if key in self._attr():
719             index = key
720             if not isinstance(self._attr(), dict):
721                 index = self._attr().index(item)
722             if hasattr(item, '_add'):
723                 self._attr()[index]._add = False
724             if hasattr(item, '_delete'):
725                 self._attr()[index]._delete = False
726         else: # it's *really* new
727             if isinstance(self._attr(), dict):
728                 self._attr()[key] = item
729             else:
730                 self._attr().append(item)
731
732     @handler(u'Update an item')
733     def update(self, index, *args, **kwargs):
734         r"update(index, ...) -> None :: Update an item of the container."
735         # TODO make it right with metaclasses, so the method is not created
736         # unless the update() method really exists.
737         # TODO check if the modified item is the same of an existing one
738         if not isinstance(self._attr(), dict):
739             index = int(index) # TODO validation
740         if not hasattr(self._cont_subhandler_class, 'update'):
741             raise CommandNotFoundError(('update',))
742         try:
743             item = self._vattr()[index]
744             item.update(*args, **kwargs)
745             if hasattr(item, '_update'):
746                 item._update = True
747         except LookupError:
748             raise ItemNotFoundError(index)
749
750     @handler(u'Delete an item')
751     def delete(self, index):
752         r"delete(index) -> None :: Delete an item of the container."
753         if not isinstance(self._attr(), dict):
754             index = int(index) # TODO validation
755         try:
756             item = self._vattr()[index]
757             if hasattr(item, '_delete'):
758                 item._delete = True
759             else:
760                 del self._attr()[index]
761             return item
762         except LookupError:
763             raise ItemNotFoundError(index)
764
765     @handler(u'Remove all items (use with care).')
766     def clear(self):
767         r"clear() -> None :: Delete all items of the container."
768         if isinstance(self._attr(), dict):
769             self._attr.clear()
770         else:
771             self._attr(list())
772
773     @handler(u'Get information about an item')
774     def get(self, index):
775         r"get(index) -> item :: List all the information of an item."
776         if not isinstance(self._attr(), dict):
777             index = int(index) # TODO validation
778         try:
779             return self._vattr()[index]
780         except LookupError:
781             raise ItemNotFoundError(index)
782
783     @handler(u'Get information about all items')
784     def show(self):
785         r"show() -> list of items :: List all the complete items information."
786         if isinstance(self._attr(), dict):
787             return self._attr().values()
788         return self._vattr()
789
790 class ListSubHandler(ContainerSubHandler):
791     r"""ListSubHandler(parent) -> ListSubHandler instance.
792
793     ContainerSubHandler holding lists. See ComposedSubHandler documentation
794     for details.
795     """
796
797     @handler(u'Get how many items are in the list')
798     def len(self):
799         r"len() -> int :: Get how many items are in the list."
800         return len(self._vattr())
801
802 class DictSubHandler(ContainerSubHandler):
803     r"""DictSubHandler(parent) -> DictSubHandler instance.
804
805     ContainerSubHandler holding dicts. See ComposedSubHandler documentation
806     for details.
807     """
808
809     @handler(u'List all the items by key')
810     def list(self):
811         r"list() -> tuple :: List all the item keys."
812         return self._attr().keys()
813
814 class ComposedSubHandler(SubHandler):
815     r"""ComposedSubHandler(parent) -> ComposedSubHandler instance.
816
817     This is a helper class to implement ListComposedSubHandler and
818     DictComposedSubHandler. You should not use it directly.
819
820     This class is usefull when you have a parent that has a dict (cont)
821     that stores some object that has an attribute (attr) with a list or
822     a dict of objects of some class. In that case, this class provides
823     automated commands to add, update, delete, get and show that objects.
824     This commands takes the cont (key of the dict for the object holding
825     the attr), and an index for access the object itself (in the attr
826     list/dict).
827
828     The container object (cont) that holds a containers, the attribute of
829     that object that is the container itself, and the class of the objects
830     that it contains can be defined by calling the constructor or in a
831     more declarative way as class attributes, like:
832
833     class TestHandler(ComposedSubHandler):
834         _comp_subhandler_cont = 'some_cont'
835         _comp_subhandler_attr = 'some_attr'
836         _comp_subhandler_class = SomeClass
837
838     This way, the parent's some_cont attribute (self.parent.some_cont)
839     will be managed automatically, providing the commands: add, update,
840     delete, get and show for manipulating a particular instance that holds
841     of SomeClass. For example, updating an item at the index 5 is the same
842     (simplified) as doing parent.some_cont[cont][5].update().
843     SomeClass should provide a cmp operator to see if the item is on the
844     container and an update() method, if it should be possible to modify
845     it. If SomeClass has an _add, _update or _delete attribute, it set
846     them to true when the item is added, updated or deleted respectively
847     (in case that it's deleted, it's not removed from the container,
848     but it's not listed either). If the container objects
849     (parent.some_cont[cont]) has an _update attribute, it's set to True
850     when any add, update or delete command is executed.
851     """
852
853     def __init__(self, parent, cont=None, attr=None, cls=None):
854         r"Initialize the object, see the class documentation for details."
855         self.parent = parent
856         if cont is not None:
857             self._comp_subhandler_cont = cont
858         if attr is not None:
859             self._comp_subhandler_attr = attr
860         if cls is not None:
861             self._comp_subhandler_class = cls
862
863     def _cont(self):
864         return getattr(self.parent, self._comp_subhandler_cont)
865
866     def _attr(self, cont, attr=None):
867         if attr is None:
868             return getattr(self._cont()[cont], self._comp_subhandler_attr)
869         setattr(self._cont()[cont], self._comp_subhandler_attr, attr)
870
871     def _vattr(self, cont):
872         if isinstance(self._attr(cont), dict):
873             return dict([(k, i) for (k, i) in self._attr(cont).items()
874                     if not hasattr(i, '_delete') or not i._delete])
875         return [i for i in self._attr(cont)
876                 if not hasattr(i, '_delete') or not i._delete]
877
878     @handler(u'Add a new item')
879     def add(self, cont, *args, **kwargs):
880         r"add(cont, ...) -> None :: Add an item to the list."
881         if not cont in self._cont():
882             raise ContainerNotFoundError(cont)
883         item = self._comp_subhandler_class(*args, **kwargs)
884         if hasattr(item, '_add'):
885             item._add = True
886         key = item
887         if isinstance(self._attr(cont), dict):
888             key = item.as_tuple()[0]
889         # do we have the same item? then raise an error
890         if key in self._vattr(cont):
891             raise ItemAlreadyExistsError(item)
892         # do we have the same item, but logically deleted? then update flags
893         if key in self._attr(cont):
894             index = key
895             if not isinstance(self._attr(cont), dict):
896                 index = self._attr(cont).index(item)
897             if hasattr(item, '_add'):
898                 self._attr(cont)[index]._add = False
899             if hasattr(item, '_delete'):
900                 self._attr(cont)[index]._delete = False
901         else: # it's *really* new
902             if isinstance(self._attr(cont), dict):
903                 self._attr(cont)[key] = item
904             else:
905                 self._attr(cont).append(item)
906         if hasattr(self._cont()[cont], '_update'):
907             self._cont()[cont]._update = True
908
909     @handler(u'Update an item')
910     def update(self, cont, index, *args, **kwargs):
911         r"update(cont, index, ...) -> None :: Update an item of the container."
912         # TODO make it right with metaclasses, so the method is not created
913         # unless the update() method really exists.
914         # TODO check if the modified item is the same of an existing one
915         if not cont in self._cont():
916             raise ContainerNotFoundError(cont)
917         if not isinstance(self._attr(cont), dict):
918             index = int(index) # TODO validation
919         if not hasattr(self._comp_subhandler_class, 'update'):
920             raise CommandNotFoundError(('update',))
921         try:
922             item = self._vattr(cont)[index]
923             item.update(*args, **kwargs)
924             if hasattr(item, '_update'):
925                 item._update = True
926             if hasattr(self._cont()[cont], '_update'):
927                 self._cont()[cont]._update = True
928         except LookupError:
929             raise ItemNotFoundError(index)
930
931     @handler(u'Delete an item')
932     def delete(self, cont, index):
933         r"delete(cont, index) -> None :: Delete an item of the container."
934         if not cont in self._cont():
935             raise ContainerNotFoundError(cont)
936         if not isinstance(self._attr(cont), dict):
937             index = int(index) # TODO validation
938         try:
939             item = self._vattr(cont)[index]
940             if hasattr(item, '_delete'):
941                 item._delete = True
942             else:
943                 del self._attr(cont)[index]
944             if hasattr(self._cont()[cont], '_update'):
945                 self._cont()[cont]._update = True
946             return item
947         except LookupError:
948             raise ItemNotFoundError(index)
949
950     @handler(u'Remove all items (use with care).')
951     def clear(self, cont):
952         r"clear(cont) -> None :: Delete all items of the container."
953         if not cont in self._cont():
954             raise ContainerNotFoundError(cont)
955         if isinstance(self._attr(cont), dict):
956             self._attr(cont).clear()
957         else:
958             self._attr(cont, list())
959
960     @handler(u'Get information about an item')
961     def get(self, cont, index):
962         r"get(cont, index) -> item :: List all the information of an item."
963         if not cont in self._cont():
964             raise ContainerNotFoundError(cont)
965         if not isinstance(self._attr(cont), dict):
966             index = int(index) # TODO validation
967         try:
968             return self._vattr(cont)[index]
969         except LookupError:
970             raise ItemNotFoundError(index)
971
972     @handler(u'Get information about all items')
973     def show(self, cont):
974         r"show(cont) -> list of items :: List all the complete items information."
975         if not cont in self._cont():
976             raise ContainerNotFoundError(cont)
977         if isinstance(self._attr(cont), dict):
978             return self._attr(cont).values()
979         return self._vattr(cont)
980
981 class ListComposedSubHandler(ComposedSubHandler):
982     r"""ListComposedSubHandler(parent) -> ListComposedSubHandler instance.
983
984     ComposedSubHandler holding lists. See ComposedSubHandler documentation
985     for details.
986     """
987
988     @handler(u'Get how many items are in the list')
989     def len(self, cont):
990         r"len(cont) -> int :: Get how many items are in the list."
991         if not cont in self._cont():
992             raise ContainerNotFoundError(cont)
993         return len(self._vattr(cont))
994
995 class DictComposedSubHandler(ComposedSubHandler):
996     r"""DictComposedSubHandler(parent) -> DictComposedSubHandler instance.
997
998     ComposedSubHandler holding dicts. See ComposedSubHandler documentation
999     for details.
1000     """
1001
1002     @handler(u'List all the items by key')
1003     def list(self, cont):
1004         r"list(cont) -> tuple :: List all the item keys."
1005         if not cont in self._cont():
1006             raise ContainerNotFoundError(cont)
1007         return self._attr(cont).keys()
1008
1009
1010 if __name__ == '__main__':
1011
1012     # Execution tests
1013     class STestHandler1(ServiceHandler):
1014         _service_start = ('service', 'start')
1015         _service_stop = ('service', 'stop')
1016         _service_restart = ('ls', '/')
1017         _service_reload = ('cp', '/la')
1018     class STestHandler2(ServiceHandler):
1019         def __init__(self):
1020             ServiceHandler.__init__(self, 'cmd-start', 'cmd-stop',
1021                                         'cmd-restart', 'cmd-reload')
1022     class ITestHandler1(InitdHandler):
1023         _initd_name = 'test1'
1024     class ITestHandler2(InitdHandler):
1025         def __init__(self):
1026             InitdHandler.__init__(self, 'test2', '/usr/local/etc/init.d')
1027     handlers = [
1028         STestHandler1(),
1029         STestHandler2(),
1030         ITestHandler1(),
1031         ITestHandler2(),
1032     ]
1033     for h in handlers:
1034         print h.__class__.__name__
1035         try:
1036             h.start()
1037         except ExecutionError, e:
1038             print e
1039         try:
1040             h.stop()
1041         except ExecutionError, e:
1042             print e
1043         try:
1044             h.restart()
1045         except ExecutionError, e:
1046             print e
1047         try:
1048             h.reload()
1049         except ExecutionError, e:
1050             print e
1051         print
1052
1053     # Persistent test
1054     print 'PTestHandler'
1055     class PTestHandler(Persistent):
1056         _persistent_attrs = 'vars'
1057         def __init__(self):
1058             self.vars = dict(a=1, b=2)
1059     h = PTestHandler()
1060     print h.vars
1061     h._dump()
1062     h.vars['x'] = 100
1063     print h.vars
1064     h._load()
1065     print h.vars
1066     h.vars['x'] = 100
1067     h._dump()
1068     print h.vars
1069     del h.vars['x']
1070     print h.vars
1071     h._load()
1072     print h.vars
1073     print
1074
1075     # Restorable test
1076     print 'RTestHandler'
1077     class RTestHandler(Restorable):
1078         _persistent_attrs = 'vars'
1079         _restorable_defaults = dict(vars=dict(a=1, b=2))
1080         def __init__(self):
1081             self._restore()
1082     h = RTestHandler()
1083     print h.vars
1084     h.vars['x'] = 100
1085     h._dump()
1086     h = RTestHandler()
1087     print h.vars
1088     print
1089
1090     # ConfigWriter test
1091     print 'CTestHandler'
1092     import os
1093     os.mkdir('templates')
1094     f = file('templates/config', 'w')
1095     f.write('Hello, ${name}! You are ${what}.')
1096     f.close()
1097     print 'template:'
1098     print file('templates/config').read()
1099     class CTestHandler(ConfigWriter):
1100         _config_writer_files = 'config'
1101         def __init__(self):
1102             self._config_build_templates()
1103         def _get_config_vars(self, config_file):
1104             return dict(name='you', what='a parrot')
1105     h = CTestHandler()
1106     h._write_config()
1107     print 'config:'
1108     print file('config').read()
1109     os.unlink('config')
1110     os.unlink('templates/config')
1111     os.rmdir('templates')
1112     print
1113