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