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