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