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