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