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