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