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