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