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