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