]> git.llucax.com Git - software/pymin.git/commitdiff
Factored out a lot of common code.
authorLeandro Lucarella <llucarella@integratech.com.ar>
Wed, 3 Oct 2007 21:41:48 +0000 (18:41 -0300)
committerLeandro Lucarella <llucarella@integratech.com.ar>
Wed, 3 Oct 2007 21:41:48 +0000 (18:41 -0300)
A new module is created: services.util which has a lot of helper classes
and functions to do common tasks of service handlers. Actual classes are:
Persistent, Restorable, ConfigWriter, ServiceHandler, InitdHandler and
TransactionalHandler (see documentation for details).

A call() function is provided as a simple wrapper for subprocess.call() to
execute simple commands. It can raise and ExecutionError, as a wrapper for
subprocess.call exceptions or ReturnNot0Error if the returned value is not
0.

All actual service handlers were updated.

16 files changed:
TODO
config.py
services/__init__.py
services/dhcp/__init__.py
services/dns/__init__.py
services/firewall/__init__.py
services/ip/__init__.py
services/ip/templates/device [moved from services/ip/templates/device.command with 100% similarity]
services/ip/templates/ip_add [moved from services/ip/templates/ip_add.command with 100% similarity]
services/ip/templates/ip_del [moved from services/ip/templates/ip_del.command with 100% similarity]
services/ip/templates/ip_flush [moved from services/ip/templates/ip_flush.command with 100% similarity]
services/ip/templates/route_add [moved from services/ip/templates/route_add.command with 100% similarity]
services/ip/templates/route_del [moved from services/ip/templates/route_del.command with 100% similarity]
services/ip/templates/route_flush [moved from services/ip/templates/route_flush.command with 100% similarity]
services/proxy/__init__.py
services/util.py [new file with mode: 0644]

diff --git a/TODO b/TODO
index 18d2325e3265bb690e54be4f07b9a46b2def81da..362b434a91b895c8e25d83965ca2bf00dc3d831c 100644 (file)
--- a/TODO
+++ b/TODO
@@ -18,6 +18,18 @@ Ideas / TODO:
 
 * Agregar validación con formencode.
 
+* Ver como manejar la información sobre si un servicio está andando o no. Si se
+  agrega una acción 'status' para ver el estado y si ese estado se saca de posta
+  de /proc o si es un estado interno y se asume que los servicios no se caen (no
+  creo que sea una buena idea esto último). Además habría que ver cuando arranca
+  el pymin, si se inician servicios automáticamente o no y si la info de qué
+  servicios iniciar o no es persistente y si puede configurarla el usuario.
+
+* No usar comandos con templates, porque después si no hay que ejecutarlos con
+  un shell (porque el template devuelve un string todo grande) y hay que andar
+  teniendo cuidado de escapar las cosas (y hay riesgos de seguridad de shell
+  injection).
+
 Estas cosas quedan sujetas a necesitada y a definición del protocolo.
 Para mí lo ideal es que el protocolo de red sea igual que la consola del
 usuario, porque después de todo no va a ser más que eso, mandar comanditos.
index f155fcb7dbd47d1701fac903fa0ef0c43e7eeab6..a0f1c45dd1bc855ed69dc7208428da8d0c75532d 100644 (file)
--- a/config.py
+++ b/config.py
@@ -26,6 +26,10 @@ routes = dict \
         pickle_dir = 'var/lib/pymin/pickle/ip',
         config_dir = 'var/lib/pymin/config/ip',
     ),
+    proxy = ProxyHandler(
+        pickle_dir = 'var/lib/pymin/pickle/proxy',
+        config_dir = 'var/lib/pymin/config/proxy',
+    ),
 )
 
 bind_addr = \
index bd7c3c1ad08710e03afc7b8b9098b1c2ed0b06f3..4d4dfb66d0125fb873937fa074d5031d1fee74f5 100644 (file)
@@ -4,4 +4,5 @@ from services.dhcp import DhcpHandler
 from services.dns import DnsHandler
 from services.firewall import FirewallHandler
 from services.ip import IpHandler
+from services.proxy import ProxyHandler
 
index 08bb8b583255dd74a2cb3d29288e9df1f3c3e0c9..7ffadcf7a4dc424a421bb6d77fc2b71d8df8f76e 100644 (file)
@@ -1,40 +1,15 @@
 # vim: set encoding=utf-8 et sw=4 sts=4 :
 
-from mako.template import Template
-from mako.runtime import Context
 from os import path
-try:
-    import cPickle as pickle
-except ImportError:
-    import pickle
-
-try:
-    from seqtools import Sequence
-except ImportError:
-    # NOP for testing
-    class Sequence: pass
-try:
-    from dispatcher import Handler, handler, HandlerError
-except ImportError:
-    # NOP for testing
-    class HandlerError(RuntimeError): pass
-    class Handler: pass
-    def handler(help):
-        def wrapper(f):
-            return f
-        return wrapper
+
+from seqtools import Sequence
+from dispatcher import Handler, handler, HandlerError
+from services.util import Restorable, ConfigWriter
+from services.util import InitdHandler, TransactionalHandler
 
 __ALL__ = ('DhcpHandler', 'Error', 'HostError', 'HostAlreadyExistsError',
             'HostNotFoundError', 'ParameterError', 'ParameterNotFoundError')
 
-pickle_ext = '.pkl'
-pickle_vars = 'vars'
-pickle_hosts = 'hosts'
-
-config_filename = 'dhcpd.conf'
-
-template_dir = path.join(path.dirname(__file__), 'templates')
-
 class Error(HandlerError):
     r"""
     Error(message) -> Error instance :: Base DhcpHandler exception class.
@@ -167,32 +142,22 @@ class HostHandler(Handler):
 
     @handler(u'Get information about a host.')
     def get(self, name):
-        r"""get(name) -> CSV string :: List all the information of a host.
-
-        The host is returned as a CSV list of: hostname,ip,mac
-        """
+        r"get(name) -> Host :: List all the information of a host."
         if not name in self.hosts:
             raise HostNotFoundError(name)
         return self.hosts[name]
 
     @handler(u'List hosts.')
     def list(self):
-        r"""list() -> CSV string :: List all the hostnames.
-
-        The list is returned as a single CSV line with all the hostnames.
-        """
+        r"list() -> tuple :: List all the hostnames."
         return self.hosts.keys()
 
     @handler(u'Get information about all hosts.')
     def show(self):
-        r"""show() -> CSV string :: List all the complete hosts information.
-
-        The hosts are returned as a CSV list with each host in a line, like:
-        hostname,ip,mac
-        """
+        r"show() -> list of Hosts :: List all the complete hosts information."
         return self.hosts.values()
 
-class DhcpHandler(Handler):
+class DhcpHandler(Restorable, ConfigWriter, InitdHandler, TransactionalHandler):
     r"""DhcpHandler([pickle_dir[, config_dir]]) -> DhcpHandler instance.
 
     Handles DHCP service commands for the dhcpd program.
@@ -204,19 +169,13 @@ class DhcpHandler(Handler):
     Both defaults to the current working directory.
     """
 
-    def __init__(self, pickle_dir='.', config_dir='.'):
-        r"Initialize DhcpHandler object, see class documentation for details."
-        self.pickle_dir = pickle_dir
-        self.config_dir = config_dir
-        filename = path.join(template_dir, config_filename)
-        self.template = Template(filename=filename)
-        try:
-            self._load()
-        except IOError:
-            # This is the first time the handler is used, create a basic
-            # setup using some nice defaults
-            self.hosts = dict()
-            self.vars = dict(
+    _initd_name = 'dhcpd'
+
+    _persistent_vars = ('vars', 'hosts')
+
+    _restorable_defaults = dict(
+            hosts = dict(),
+            vars  = dict(
                 domain_name = 'example.com',
                 dns_1       = 'ns1.example.com',
                 dns_2       = 'ns2.example.com',
@@ -225,11 +184,23 @@ class DhcpHandler(Handler):
                 net_start   = '192.168.0.100',
                 net_end     = '192.168.0.200',
                 net_gateway = '192.168.0.1',
-            )
-            self._dump()
-            self._write_config()
+            ),
+    )
+
+    _config_writer_files = 'dhcpd.conf'
+    _config_writer_tpl_dir = path.join(path.dirname(__file__), 'templates')
+
+    def __init__(self, pickle_dir='.', config_dir='.'):
+        r"Initialize DhcpHandler object, see class documentation for details."
+        self._persistent_dir = pickle_dir
+        self._config_writer_cfg_dir = config_dir
+        self._config_build_templates()
+        self._restore()
         self.host = HostHandler(self.hosts)
 
+    def _get_config_vars(self, config_file):
+        return dict(hosts=self.hosts.values(), **self.vars)
+
     @handler(u'Set a DHCP parameter.')
     def set(self, param, value):
         r"set(param, value) -> None :: Set a DHCP parameter."
@@ -246,138 +217,47 @@ class DhcpHandler(Handler):
 
     @handler(u'List all available DHCP parameters.')
     def list(self):
-        r"""list() -> CSV string :: List all the parameter names.
-
-        The list is returned as a single CSV line with all the names.
-        """
+        r"list() -> tuple :: List all the parameter names."
         return self.vars.keys()
 
     @handler(u'Get all DHCP parameters, with their values.')
     def show(self):
-        r"""show() -> CSV string :: List all the parameters (with their values).
-
-        The parameters are returned as a CSV list with each parameter in a
-        line, like:
-        name,value
-        """
+        r"show() -> (key, value) tuples :: List all the parameters."
         return self.vars.items()
 
-    @handler(u'Start the service.')
-    def start(self):
-        r"start() -> None :: Start the DHCP service."
-        #esto seria para poner en una interfaz
-        #y seria el hook para arrancar el servicio
-        pass
-
-    @handler(u'Stop the service.')
-    def stop(self):
-        r"stop() -> None :: Stop the DHCP service."
-        #esto seria para poner en una interfaz
-        #y seria el hook para arrancar el servicio
-        pass
-
-    @handler(u'Restart the service.')
-    def restart(self):
-        r"restart() -> None :: Restart the DHCP service."
-        #esto seria para poner en una interfaz
-        #y seria el hook para arrancar el servicio
-        pass
-
-    @handler(u'Reload the service config (without restarting, if possible).')
-    def reload(self):
-        r"reload() -> None :: Reload the configuration of the DHCP service."
-        #esto seria para poner en una interfaz
-        #y seria el hook para arrancar el servicio
-        pass
-
-    @handler(u'Commit the changes (reloading the service, if necessary).')
-    def commit(self):
-        r"commit() -> None :: Commit the changes and reload the DHCP service."
-        #esto seria para poner en una interfaz
-        #y seria que hace el pickle deberia llamarse
-        #al hacerse un commit
-        self._dump()
-        self._write_config()
-        self.reload()
-
-    @handler(u'Discard all the uncommited changes.')
-    def rollback(self):
-        r"rollback() -> None :: Discard the changes not yet commited."
-        self._load()
-
-    def _dump(self):
-        r"_dump() -> None :: Dump all persistent data to pickle files."
-        # XXX podría ir en una clase base
-        self._dump_var(self.vars, pickle_vars)
-        self._dump_var(self.hosts, pickle_hosts)
-
-    def _load(self):
-        r"_load() -> None :: Load all persistent data from pickle files."
-        # XXX podría ir en una clase base
-        self.vars = self._load_var(pickle_vars)
-        self.hosts = self._load_var(pickle_hosts)
-
-    def _pickle_filename(self, name):
-        r"_pickle_filename() -> string :: Construct a pickle filename."
-        # XXX podría ir en una clase base
-        return path.join(self.pickle_dir, name) + pickle_ext
-
-    def _dump_var(self, var, name):
-        r"_dump_var() -> None :: Dump a especific variable to a pickle file."
-        # XXX podría ir en una clase base
-        pkl_file = file(self._pickle_filename(name), 'wb')
-        pickle.dump(var, pkl_file, 2)
-        pkl_file.close()
-
-    def _load_var(self, name):
-        r"_load_var() -> object :: Load a especific pickle file."
-        # XXX podría ir en una clase base
-        return pickle.load(file(self._pickle_filename(name)))
-
-    def _write_config(self):
-        r"_write_config() -> None :: Generate all the configuration files."
-        # XXX podría ir en una clase base, ver como generalizar variables a
-        # reemplazar en la template
-        out_file = file(path.join(self.config_dir, config_filename), 'w')
-        ctx = Context(out_file, hosts=self.hosts.values(), **self.vars)
-        self.template.render_context(ctx)
-        out_file.close()
-
 if __name__ == '__main__':
 
     import os
 
-    dhcp_handler = DhcpHandler()
+    h = DhcpHandler()
 
     def dump():
         print '-' * 80
-        print 'Variables:', dhcp_handler.list()
-        print dhcp_handler.show()
+        print 'Variables:', h.list()
+        print h.show()
         print
-        print 'Hosts:', dhcp_handler.host.list()
-        print dhcp_handler.host.show()
+        print 'Hosts:', h.host.list()
+        print h.host.show()
         print '-' * 80
 
     dump()
 
-    dhcp_handler.host.add('my_name','192.168.0.102','00:12:ff:56')
+    h.host.add('my_name','192.168.0.102','00:12:ff:56')
 
-    dhcp_handler.host.update('my_name','192.168.0.192','00:12:ff:56')
+    h.host.update('my_name','192.168.0.192','00:12:ff:56')
 
-    dhcp_handler.host.add('nico','192.168.0.188','00:00:00:00')
+    h.host.add('nico','192.168.0.188','00:00:00:00')
 
-    dhcp_handler.set('domain_name','baryon.com.ar')
+    h.set('domain_name','baryon.com.ar')
 
     try:
-        dhcp_handler.set('sarasa','baryon.com.ar')
+        h.set('sarasa','baryon.com.ar')
     except KeyError, e:
         print 'Error:', e
 
-    dhcp_handler.commit()
+    h.commit()
 
     dump()
 
-    for f in (pickle_vars + pickle_ext, pickle_hosts + pickle_ext,
-                                                            config_filename):
-        os.unlink(f)
+    os.system('rm -f *.pkl ' + ' '.join(h._config_writer_files))
 
index 8f08d35328b37dac1db8fa8eeae17db4ae88dec8..e3739e382a0767784c92e0a5fcdcf7f5b930550c 100644 (file)
@@ -1,44 +1,22 @@
 # vim: set encoding=utf-8 et sw=4 sts=4 :
 
 # TODO COMMENT
-from mako.template import Template
-from mako.runtime import Context
 from os import path
 from os import unlink
+from new import instancemethod
 
-try:
-    import cPickle as pickle
-except ImportError:
-    import pickle
+from seqtools import Sequence
+from dispatcher import handler, HandlerError, Handler
+from services.util import Restorable, ConfigWriter, call
+from services.util import InitdHandler, TransactionalHandler
 
-try:
-    from seqtools import Sequence
-except ImportError:
-    # NOP for testing
-    class Sequence: pass
-
-try:
-    from dispatcher import handler, HandlerError, Handler
-except ImportError:
-    class HandlerError(RuntimeError): pass
-    class Handler: pass
-    def handler(help):
-        def wrapper(f):
-            return f
-        return wrapper
-
-
-
-__ALL__ = ('DnsHandler',)
-
-pickle_ext = '.pkl'
-
-pickle_vars = 'vars'
-pickle_zones = 'zones'
-
-config_filename = 'named.conf'
-zone_filename = 'zoneX.zone'
-zone_filename_ext = '.zone'
+__ALL__ = ('DnsHandler', 'Error',
+            'ZoneError', 'ZoneNotFoundError', 'ZoneAlreadyExistsError',
+            'HostError', 'HostAlreadyExistsError', 'HostNotFoundError',
+            'MailExchangeError', 'MailExchangeAlreadyExistsError',
+            'MailExchangeNotFoundError', 'NameServerError',
+            'NameServerAlreadyExistsError', 'NameServerNotFoundError',
+            'ParameterError', 'ParameterNotFoundError')
 
 template_dir = path.join(path.dirname(__file__), 'templates')
 
@@ -418,7 +396,7 @@ class ZoneHandler(Handler):
     def show(self):
         return self.zones.values()
 
-class DnsHandler(Handler):
+class DnsHandler(Restorable, ConfigWriter, InitdHandler, TransactionalHandler):
     r"""DnsHandler([pickle_dir[, config_dir]]) -> DnsHandler instance.
 
     Handles DNS service commands for the dns program.
@@ -430,30 +408,34 @@ class DnsHandler(Handler):
     Both defaults to the current working directory.
     """
 
-    def __init__(self, pickle_dir='.', config_dir='.'):
-        r"Initialize DnsHandler object, see class documentation for details."
-        self.pickle_dir = pickle_dir
-        self.config_dir = config_dir
-        c_filename = path.join(template_dir, config_filename)
-        z_filename = path.join(template_dir, zone_filename)
-        self.config_template = Template(filename=c_filename)
-        self.zone_template = Template(filename=z_filename)
-        try :
-            self._load()
-        except IOError:
-            self.zones = dict()
-            self.vars = dict(
+    _initd_name = 'bind'
+
+    _persistent_vars = ('vars', 'zones')
+
+    _restorable_defaults = dict(
+            zones = dict(),
+            vars  = dict(
                 isp_dns1 = '',
                 isp_dns2 = '',
                 bind_addr1 = '',
                 bind_addr2 = ''
-            )
+            ),
+    )
+
+    _config_writer_files = ('named.conf', 'zoneX.zone')
+    _config_writer_tpl_dir = path.join(path.dirname(__file__), 'templates')
 
+    def __init__(self, pickle_dir='.', config_dir='.'):
+        r"Initialize DnsHandler object, see class documentation for details."
+        self._persistent_dir = pickle_dir
+        self._config_writer_cfg_dir = config_dir
+        self.mod = False
+        self._config_build_templates()
+        self._restore()
         self.host = HostHandler(self.zones)
         self.zone = ZoneHandler(self.zones)
         self.mx = MailExchangeHandler(self.zones)
         self.ns = NameServerHandler(self.zones)
-        self.mod = False
 
     @handler(u'Set a DNS parameter')
     def set(self, param, value):
@@ -478,103 +460,32 @@ class DnsHandler(Handler):
     def show(self):
         return self.vars.values()
 
-    @handler(u'Start the service.')
-    def start(self):
-        r"start() -> None :: Start the DNS service."
-        #esto seria para poner en una interfaz
-        #y seria el hook para arrancar el servicio
-        pass
-
-    @handler(u'Stop the service.')
-    def stop(self):
-        r"stop() -> None :: Stop the DNS service."
-        #esto seria para poner en una interfaz
-        #y seria el hook para arrancar el servicio
-        pass
-
-    @handler(u'Restart the service.')
-    def restart(self):
-        r"restart() -> None :: Restart the DNS service."
-        #esto seria para poner en una interfaz
-        #y seria el hook para arrancar el servicio
-        pass
-
-    @handler(u'Reload the service config (without restarting, if possible)')
-    def reload(self):
-        r"reload() -> None :: Reload the configuration of the DNS service."
-        #esto seria para poner en una interfaz
-        #y seria el hook para arrancar el servicio
-        pass
-
-    @handler(u'Commit the changes (reloading the service, if necessary).')
-    def commit(self):
-        r"commit() -> None :: Commit the changes and reload the DNS service."
-        #esto seria para poner en una interfaz
-        #y seria que hace el pickle deberia llamarse
-        #al hacerse un commit
-        self._dump()
-        self._write_config()
-        self.reload()
-
-    @handler(u'Discard all the uncommited changes.')
-    def rollback(self):
-        r"rollback() -> None :: Discard the changes not yet commited."
-        self._load()
-
-    def _dump(self):
-        r"_dump() -> None :: Dump all persistent data to pickle files."
-        # XXX podría ir en una clase base
-        self._dump_var(self.vars, pickle_vars)
-        self._dump_var(self.zones, pickle_zones)
-
-    def _load(self):
-        r"_load() -> None :: Load all persistent data from pickle files."
-        # XXX podría ir en una clase base
-        self.vars = self._load_var(pickle_vars)
-        self.zones = self._load_var(pickle_zones)
-
-    def _pickle_filename(self, name):
-        r"_pickle_filename() -> string :: Construct a pickle filename."
-        # XXX podría ir en una clase base
-        return path.join(self.pickle_dir, name) + pickle_ext
-
-    def _dump_var(self, var, name):
-        r"_dump_var() -> None :: Dump a especific variable to a pickle file."
-        # XXX podría ir en una clase base
-        pkl_file = file(self._pickle_filename(name), 'wb')
-        pickle.dump(var, pkl_file, 2)
-        pkl_file.close()
-
-    def _load_var(self, name):
-        r"_load_var() -> object :: Load a especific pickle file."
-        # XXX podría ir en una clase base
-        return pickle.load(file(self._pickle_filename(name)))
+    def _zone_filename(self, zone):
+        return zone.name + '.zone'
+
+    def _get_config_vars(self, config_file):
+        return dict(zones=self.zones.values(), **self.vars)
 
     def _write_config(self):
         r"_write_config() -> None :: Generate all the configuration files."
-        # XXX podría ir en una clase base, ver como generalizar variables a
-        # reemplazar en la template
-        #archivos de zona
         delete_zones = list()
         for a_zone in self.zones.values():
             if a_zone.mod:
                 if not a_zone.new:
                     # TODO freeze de la zona
-                    print 'Freezing zone ' + a_zone.name + zone_filename_ext
-                zone_out_file = file(path.join(self.config_dir, a_zone.name + zone_filename_ext), 'w')
-                ctx = Context(
-                    zone_out_file,
+                    call(('dns', 'freeze', a_zone.name))
+                vars = dict(
                     zone = a_zone,
                     hosts = a_zone.hosts.values(),
                     mxs = a_zone.mxs.values(),
                     nss = a_zone.nss.values()
-                    )
-                self.zone_template.render_context(ctx)
-                zone_out_file.close()
+                )
+                self._write_single_config('zoneX.zone',
+                                            self._zone_filename(a_zone), vars)
                 a_zone.mod = False
                 if not a_zone.new:
                     # TODO unfreeze de la zona
-                    print 'Unfreezing zone ' + a_zone.name + zone_filename_ext
+                    call(('dns', 'unfreeze', a_zone.name))
                 else :
                     self.mod = True
                     a_zone.new = False
@@ -582,7 +493,7 @@ class DnsHandler(Handler):
                 #borro el archivo .zone
                 try:
                     self.mod = True
-                    unlink(path.join(self.config_dir, a_zone.name + zone_filename_ext))
+                    unlink(self._zone_filename(a_zone))
                 except OSError:
                     #la excepcion pude darse en caso que haga un add de una zona y
                     #luego el del, como no hice commit, no se crea el archivo
@@ -592,15 +503,10 @@ class DnsHandler(Handler):
         for z in delete_zones:
             del self.zones[z]
         #archivo general
-        if self.mod :
-            cfg_out_file = file(path.join(self.config_dir, config_filename), 'w')
-            ctx = Context(cfg_out_file, zones=self.zones.values(), **self.vars)
-            self.config_template.render_context(ctx)
-            cfg_out_file.close()
+        if self.mod:
+            self._write_single_config('named.conf')
             self.mod = False
-            print 'Restarting service'
-
-
+            self.reload()
 
 if __name__ == '__main__':
 
@@ -642,26 +548,24 @@ if __name__ == '__main__':
 
     dns.commit()
 
-    print 'ZONAS :'
-    print dns.zone.show() + '\n'
-    print 'HOSTS :'
-    print dns.host.show()
+    print 'ZONAS :', dns.zone.show()
+    print 'HOSTS :', dns.host.show()
 
     #test zone errors
-    try:
-        dns.zone.update('zone-sarasa','lalal')
-    except ZoneNotFoundError, inst:
-        print 'Error: ', inst
+    #try:
+    #    dns.zone.update('zone-sarasa','lalal')
+    #except ZoneNotFoundError, inst:
+    #    print 'Error: ', inst
 
     try:
         dns.zone.delete('zone-sarasa')
     except ZoneNotFoundError, inst:
         print 'Error: ', inst
 
-    try:
-        dns.zone.add('zona_loca.com','ns1.dom.com','ns2.dom.com')
-    except ZoneAlreadyExistsError, inst:
-        print 'Error: ', inst
+    #try:
+    #    dns.zone.add('zona_loca.com','ns1.dom.com','ns2.dom.com')
+    #except ZoneAlreadyExistsError, inst:
+    #    print 'Error: ', inst
 
     #test hosts errors
     try:
index 1ac1e60e6763c0b13082feaba9619195d30d0c46..bbdb3fd53f6a8943ad6ce339933660a5ae371e66 100644 (file)
@@ -3,40 +3,16 @@
 # TODO See if it's better (more secure) to execute commands via python instead
 # of using script templates.
 
-from mako.template import Template
-from mako.runtime import Context
 from os import path
-try:
-    import cPickle as pickle
-except ImportError:
-    import pickle
-
-try:
-    from seqtools import Sequence
-except ImportError:
-    # NOP for testing
-    class Sequence: pass
-try:
-    from dispatcher import Handler, handler, HandlerError
-except ImportError:
-    # NOP for testing
-    class HandlerError(RuntimeError): pass
-    class Handler: pass
-    def handler(help):
-        def wrapper(f):
-            return f
-        return wrapper
+
+from seqtools import Sequence
+from dispatcher import Handler, handler, HandlerError
+from services.util import ServiceHandler, TransactionalHandler
+from services.util import Restorable, ConfigWriter
 
 __ALL__ = ('FirewallHandler', 'Error', 'RuleError', 'RuleAlreadyExistsError',
            'RuleNotFoundError')
 
-pickle_ext = '.pkl'
-pickle_rules = 'rules'
-
-config_filename = 'iptables.sh'
-
-template_dir = path.join(path.dirname(__file__), 'templates')
-
 class Error(HandlerError):
     r"""
     Error(command) -> Error instance :: Base FirewallHandler exception class.
@@ -197,7 +173,8 @@ class RuleHandler(Handler):
         r"show() -> list of Rules :: List all the complete rules information."
         return self.rules
 
-class FirewallHandler(Handler):
+class FirewallHandler(Restorable, ConfigWriter, ServiceHandler,
+                                                        TransactionalHandler):
     r"""FirewallHandler([pickle_dir[, config_dir]]) -> FirewallHandler instance.
 
     Handles firewall commands using iptables.
@@ -209,104 +186,28 @@ class FirewallHandler(Handler):
     Both defaults to the current working directory.
     """
 
+    _persistent_vars = 'rules'
+
+    _restorable_defaults = dict(rules=list())
+
+    _config_writer_files = 'iptables.sh'
+    _config_writer_tpl_dir = path.join(path.dirname(__file__), 'templates')
+
     def __init__(self, pickle_dir='.', config_dir='.'):
-        r"Initialize FirewallHandler object, see class documentation for details."
-        self.pickle_dir = pickle_dir
-        self.config_dir = config_dir
-        filename = path.join(template_dir, config_filename)
-        self.template = Template(filename=filename)
-        try:
-            self._load()
-        except IOError:
-            # This is the first time the handler is used, create a basic
-            # setup using some nice defaults
-            self.rules = list() # TODO defaults?
-            self._dump()
-            self._write_config()
+        r"Initialize the object, see class documentation for details."
+        self._persistent_dir = pickle_dir
+        self._config_writer_cfg_dir = config_dir
+        self._service_start = path.join(self._config_writer_cfg_dir,
+                                                    self._config_writer_files)
+        self._service_stop = ('iptables', '-t', 'filter', '-F')
+        self._service_restart = self._service_start
+        self._service_reload = self._service_start
+        self._config_build_templates()
+        self._restore()
         self.rule = RuleHandler(self.rules)
 
-    # Does this (start, stop, restart, reload) makes sense??? 
-    # Implement a "try" command that apply the changes for some time and
-    # then goes back to the previous configuration if the changes are not
-    # commited. TODO
-    @handler(u'Start the service.')
-    def start(self):
-        r"start() -> None :: Start the firewall."
-        #esto seria para poner en una interfaz
-        #y seria el hook para arrancar el servicio
-        pass
-
-    @handler(u'Stop the service.')
-    def stop(self):
-        r"stop() -> None :: Stop the firewall."
-        #esto seria para poner en una interfaz
-        #y seria el hook para arrancar el servicio
-        pass
-
-    @handler(u'Restart the service.')
-    def restart(self):
-        r"restart() -> None :: Restart the firewall."
-        #esto seria para poner en una interfaz
-        #y seria el hook para arrancar el servicio
-        pass
-
-    @handler(u'Reload the service config (without restarting, if possible).')
-    def reload(self):
-        r"reload() -> None :: Reload the configuration of the firewall."
-        #esto seria para poner en una interfaz
-        #y seria el hook para arrancar el servicio
-        pass
-
-    @handler(u'Commit the changes (reloading the service, if necessary).')
-    def commit(self):
-        r"commit() -> None :: Commit the changes and reload the firewall."
-        #esto seria para poner en una interfaz
-        #y seria que hace el pickle deberia llamarse
-        #al hacerse un commit
-        self._dump()
-        self._write_config()
-        self.reload() # TODO exec the script
-
-    @handler(u'Discard all the uncommited changes.')
-    def rollback(self):
-        r"rollback() -> None :: Discard the changes not yet commited."
-        self._load()
-
-    def _dump(self):
-        r"_dump() -> None :: Dump all persistent data to pickle files."
-        # XXX podría ir en una clase base
-        self._dump_var(self.rules, pickle_rules)
-
-    def _load(self):
-        r"_load() -> None :: Load all persistent data from pickle files."
-        # XXX podría ir en una clase base
-        self.rules = self._load_var(pickle_rules)
-
-    def _pickle_filename(self, name):
-        r"_pickle_filename() -> string :: Construct a pickle filename."
-        # XXX podría ir en una clase base
-        return path.join(self.pickle_dir, name) + pickle_ext
-
-    def _dump_var(self, var, name):
-        r"_dump_var() -> None :: Dump a especific variable to a pickle file."
-        # XXX podría ir en una clase base
-        pkl_file = file(self._pickle_filename(name), 'wb')
-        pickle.dump(var, pkl_file, 2)
-        pkl_file.close()
-
-    def _load_var(self, name):
-        r"_load_var() -> object :: Load a especific pickle file."
-        # XXX podría ir en una clase base
-        return pickle.load(file(self._pickle_filename(name)))
-
-    def _write_config(self):
-        r"_write_config() -> None :: Generate all the configuration files."
-        # XXX podría ir en una clase base, ver como generalizar variables a
-        # reemplazar en la template
-        out_file = file(path.join(self.config_dir, config_filename), 'w')
-        ctx = Context(out_file, rules=self.rules)
-        self.template.render_context(ctx)
-        out_file.close()
+    def _get_config_vars(self, config_file):
+        return dict(rules=self.rules)
 
 if __name__ == '__main__':
 
@@ -330,8 +231,9 @@ if __name__ == '__main__':
 
     fw_handler.commit()
 
+    fw_handler.stop()
+
     dump()
 
-    for f in (pickle_rules + pickle_ext, config_filename):
-        os.unlink(f)
+    os.system('rm -f *.pkl iptables.sh')
 
index bc8b2759a98f27cc20fda49d9618b44399cc8dfd..cf16088fad959b8bb098432e762e202bd62ea73f 100644 (file)
@@ -1,48 +1,16 @@
 # vim: set encoding=utf-8 et sw=4 sts=4 :
 
-from mako.template import Template
-from mako.runtime import Context
 from subprocess import Popen, PIPE
 from os import path
-try:
-    import cPickle as pickle
-except ImportError:
-    import pickle
-
-try:
-    from seqtools import Sequence
-except ImportError:
-    # NOP for testing
-    class Sequence: pass
-try:
-    from dispatcher import handler, HandlerError, Handler
-except ImportError:
-    # NOP for testing
-    class HandlerError(RuntimeError): pass
-    class Handler: pass
-    def handler(help):
-        def wrapper(f):
-            return f
-        return wrapper
+
+from seqtools import Sequence
+from dispatcher import handler, HandlerError, Handler
+from services.util import Restorable, ConfigWriter
+from services.util import InitdHandler, TransactionalHandler
 
 __ALL__ = ('IpHandler','Error','DeviceError','DeviceNotFoundError','RouteError','RouteNotFoundError',
             'RouteAlreadyExistsError','AddressError','AddressNotFoundError','AddressAlreadyExistsError')
 
-pickle_ext = '.pkl'
-pickle_devices = 'devs'
-
-template_dir = path.join(path.dirname(__file__), 'templates')
-command_filename = 'command'
-
-
-device_com = 'device.command'
-ip_add_com = 'ip_add.command'
-ip_del_com = 'ip_del.command'
-ip_flush_com = 'ip_flush.command'
-route_add_com = 'route_add.command'
-route_del_com = 'route_del.command'
-route_flush_com = 'route_flush.command'
-
 class Error(HandlerError):
     r"""
     Error(command) -> Error instance :: Base IpHandler exception class.
@@ -233,8 +201,11 @@ class Device(Sequence):
 class DeviceHandler(Handler):
 
     def __init__(self, devices):
+        # FIXME remove templates to execute commands
+        from mako.template import Template
         self.devices = devices
-        dev_fn = path.join(template_dir, device_com)
+        template_dir = path.join(path.dirname(__file__), 'templates')
+        dev_fn = path.join(template_dir, 'device')
         self.device_template = Template(filename=dev_fn)
 
     @handler(u'Bring the device up')
@@ -259,114 +230,62 @@ class DeviceHandler(Handler):
     def show(self):
         return self.devices.items()
 
-class IpHandler(Handler):
+def get_devices():
+    p = Popen(('ip', 'link', 'list'), stdout=PIPE, close_fds=True)
+    string = p.stdout.read()
+    p.wait()
+    d = dict()
+    i = string.find('eth')
+    while i != -1:
+        eth = string[i:i+4]
+        m = string.find('link/ether', i+4)
+        mac = string[ m+11 : m+11+17]
+        d[eth] = Device(eth, mac)
+        i = string.find('eth', m+11+17)
+    return d
 
-    def __init__(self, pickle_dir='.', config_dir='.'):
-        r"Initialize DhcpHandler object, see class documentation for details."
+class IpHandler(Restorable, ConfigWriter, TransactionalHandler):
 
-        self.pickle_dir = pickle_dir
-        self.config_dir = config_dir
+    _persistent_vars = 'devices'
 
-        ip_add_fn = path.join(template_dir, ip_add_com)
-        ip_del_fn = path.join(template_dir, ip_del_com)
-        ip_flush_fn = path.join(template_dir, ip_flush_com)
-        self.ip_add_template = Template(filename=ip_add_fn)
-        self.ip_del_template = Template(filename=ip_del_fn)
-        self.ip_flush_template = Template(filename=ip_flush_fn)
+    _restorable_defaults = dict(devices=get_devices())
 
-        route_add_fn = path.join(template_dir, route_add_com)
-        route_del_fn = path.join(template_dir, route_del_com)
-        route_flush_fn = path.join(template_dir, route_flush_com)
-        self.route_add_template = Template(filename=route_add_fn)
-        self.route_del_template = Template(filename=route_del_fn)
-        self.route_flush_template = Template(filename=route_flush_fn)
+    _config_writer_files = ('device', 'ip_add', 'ip_del', 'ip_flush',
+                            'route_add', 'route_del', 'route_flush')
+    _config_writer_tpl_dir = path.join(path.dirname(__file__), 'templates')
 
-        try:
-            self._load()
-        except IOError:
-            p = Popen('ip link list', shell=True, stdout=PIPE, close_fds=True)
-            devs = _get_devices(p.stdout.read())
-            self.devices = dict()
-            for eth, mac in devs:
-                self.devices[eth] = Device(eth, mac)
-            self._dump()
+    def __init__(self, pickle_dir='.', config_dir='.'):
+        r"Initialize DhcpHandler object, see class documentation for details."
+        self._persistent_dir = pickle_dir
+        self._config_writer_cfg_dir = config_dir
+        self._config_build_templates()
+        self._restore()
         self.addr = AddressHandler(self.devices)
         self.route = RouteHandler(self.devices)
         self.dev = DeviceHandler(self.devices)
-        self.commit()
-
-    @handler(u'Commit the changes (reloading the service, if necessary).')
-    def commit(self):
-        r"commit() -> None :: Commit the changes and reload the DHCP service."
-        #esto seria para poner en una interfaz
-        #y seria que hace el pickle deberia llamarse
-        #al hacerse un commit
-        self._dump()
-        self._write_config()
-
-    @handler(u'Discard all the uncommited changes.')
-    def rollback(self):
-        r"rollback() -> None :: Discard the changes not yet commited."
-        self._load()
-
-    def _dump(self):
-        r"_dump() -> None :: Dump all persistent data to pickle files."
-        # XXX podría ir en una clase base
-        self._dump_var(self.devices, pickle_devices)
-
-
-    def _load(self):
-        r"_load() -> None :: Load all persistent data from pickle files."
-        # XXX podría ir en una clase base
-        self.devices = self._load_var(pickle_devices)
-
-    def _pickle_filename(self, name):
-        r"_pickle_filename() -> string :: Construct a pickle filename."
-        # XXX podría ir en una clase base
-        return path.join(self.pickle_dir, name) + pickle_ext
-
-    def _dump_var(self, var, name):
-        r"_dump_var() -> None :: Dump a especific variable to a pickle file."
-        # XXX podría ir en una clase base
-        pkl_file = file(self._pickle_filename(name), 'wb')
-        pickle.dump(var, pkl_file, 2)
-        pkl_file.close()
-
-    def _load_var(self, name):
-        r"_load_var()7 -> object :: Load a especific pickle file."
-        # XXX podría ir en una clase base
-        return pickle.load(file(self._pickle_filename(name)))
 
     def _write_config(self):
         r"_write_config() -> None :: Execute all commands."
         for device in self.devices.values():
-            print self.route_flush_template.render(dev=device.name)
-            print self.ip_flush_template.render(dev=device.name)
+            print self._render_config('route_flush', dict(dev=device.name))
+            print self._render_config('ip_flush', dict(dev=device.name))
             for address in device.addrs.values():
-                print self.ip_add_template.render(
-                    dev=device.name,
-                    addr=address.ip,
-                    prefix=address.prefix,
-                    broadcast=address.broadcast
+                print self._render_config('ip_add', dict(
+                        dev = device.name,
+                        addr = address.ip,
+                        prefix = address.prefix,
+                        broadcast = address.broadcast,
                     )
+                )
             for route in device.routes:
-                print self.route_add_template.render(
-                    dev=device.name,
-                    net_addr=route.net_addr,
-                    prefix=route.prefix,
-                    gateway=route.gateway
+                print self._render_config('route_add', dict(
+                        dev = device.name,
+                        net_addr = route.net_addr,
+                        prefix = route.prefix,
+                        gateway = route.gateway,
                     )
+                )
 
-def _get_devices(string):
-   l = list()
-   i = string.find('eth')
-   while i != -1:
-       eth = string[i:i+4]
-       m = string.find('link/ether', i+4)
-       mac = string[ m+11 : m+11+17]
-       l.append((eth,mac))
-       i = string.find('eth', m+11+17)
-   return l
 
 if __name__ == '__main__':
 
index 28735692443fb4ab5b392e9f09f2afe26fde4c50..195125c5dfbb34d5730a915207ed343a0e0555bb 100644 (file)
@@ -1,40 +1,14 @@
 # vim: set encoding=utf-8 et sw=4 sts=4 :
 
-from mako.template import Template
-from mako.runtime import Context
-from subprocess import Popen, PIPE
 from os import path
-try:
-    import cPickle as pickle
-except ImportError:
-    import pickle
-
-try:
-    from seqtools import Sequence
-except ImportError:
-    # NOP for testing
-    class Sequence: pass
-try:
-    from dispatcher import handler, HandlerError, Handler
-except ImportError:
-    # NOP for testing
-    class HandlerError(RuntimeError): pass
-    class Handler: pass
-    def handler(help):
-        def wrapper(f):
-            return f
-        return wrapper
-
-__ALL__ = ('ProxyHandler')
-
-pickle_ext = '.pkl'
-pickle_vars = 'vars'
-pickle_hosts= 'hosts'
-pickle_users = 'users'
-config_filename = 'squid.conf'
-
-template_dir = path.join(path.dirname(__file__), 'templates')
 
+from seqtools import Sequence
+from dispatcher import Handler, handler, HandlerError
+from services.util import Restorable, ConfigWriter
+from services.util import InitdHandler, TransactionalHandler
+
+__ALL__ = ('ProxyHandler', 'Error', 'HostError', 'HostAlreadyExistsError',
+            'HostNotFoundError', 'ParameterError', 'ParameterNotFoundError')
 
 class Error(HandlerError):
     r"""
@@ -144,23 +118,35 @@ class HostHandler(Handler):
         return self.hosts.items()
 
 
-class ProxyHandler(Handler):
+class ProxyHandler(Restorable, ConfigWriter, InitdHandler,
+                                            TransactionalHandler):
+
+    _initd_name = 'squid'
+
+    _persistent_vars = ('vars', 'hosts')
+
+    _restorable_defaults = dict(
+            hosts = dict(),
+            vars  = dict(
+                ip   = '192.168.0.1',
+                port = '8080',
+            ),
+    )
+
+    _config_writer_files = 'squid.conf'
+    _config_writer_tpl_dir = path.join(path.dirname(__file__), 'templates')
 
     def __init__(self, pickle_dir='.', config_dir='.'):
-        self.pickle_dir = pickle_dir
-        self.config_dir = config_dir
-        f = path.join(template_dir, config_filename)
-        self.config_template = Template(filename=f)
-        try:
-            self._load()
-        except IOError:
-            self.hosts = dict()
-            self.vars = dict(
-                    ip = '192.168.0.1',
-                    port = '8080',
-                )
+        r"Initialize DhcpHandler object, see class documentation for details."
+        self._persistent_dir = pickle_dir
+        self._config_writer_cfg_dir = config_dir
+        self._config_build_templates()
+        self._restore()
         self.host = HostHandler(self.hosts)
 
+    def _get_config_vars(self, config_file):
+        return dict(hosts=self.hosts.values(), **self.vars)
+
     @handler(u'Set a Proxy parameter')
     def set(self, param, value):
         r"set(param, value) -> None :: Set a Proxy parameter."
@@ -183,89 +169,6 @@ class ProxyHandler(Handler):
     def show(self):
         return self.vars.values()
 
-    @handler(u'Start the service.')
-    def start(self):
-        r"start() -> None :: Start the DNS service."
-        #esto seria para poner en una interfaz
-        #y seria el hook para arrancar el servicio
-        pass
-
-    @handler(u'Stop the service.')
-    def stop(self):
-        r"stop() -> None :: Stop the DNS service."
-        #esto seria para poner en una interfaz
-        #y seria el hook para arrancar el servicio
-        pass
-
-    @handler(u'Restart the service.')
-    def restart(self):
-        r"restart() -> None :: Restart the DNS service."
-        #esto seria para poner en una interfaz
-        #y seria el hook para arrancar el servicio
-        pass
-
-    @handler(u'Reload the service config (without restarting, if possible)')
-    def reload(self):
-        r"reload() -> None :: Reload the configuration of the DNS service."
-        #esto seria para poner en una interfaz
-        #y seria el hook para arrancar el servicio
-        print('reloading configuration')
-
-    @handler(u'Commit the changes (reloading the service, if necessary).')
-    def commit(self):
-        r"commit() -> None :: Commit the changes and reload the DNS service."
-        #esto seria para poner en una interfaz
-        #y seria que hace el pickle deberia llamarse
-        #al hacerse un commit
-        self._dump()
-        self._write_config()
-        self.reload()
-
-    @handler(u'Discard all the uncommited changes.')
-    def rollback(self):
-        r"rollback() -> None :: Discard the changes not yet commited."
-        self._load()
-
-    def _dump(self):
-        r"_dump() -> None :: Dump all persistent data to pickle files."
-        # XXX podría ir en una clase base
-        self._dump_var(self.vars, pickle_vars)
-        self._dump_var(self.hosts, pickle_hosts)
-
-    def _load(self):
-        r"_load() -> None :: Load all persistent data from pickle files."
-        # XXX podría ir en una clase base
-        self.vars = self._load_var(pickle_vars)
-        self.hosts = self._load_var(pickle_hosts)
-
-    def _pickle_filename(self, name):
-        r"_pickle_filename() -> string :: Construct a pickle filename."
-        # XXX podría ir en una clase base
-        return path.join(self.pickle_dir, name) + pickle_ext
-
-    def _dump_var(self, var, name):
-        r"_dump_var() -> None :: Dump a especific variable to a pickle file."
-        # XXX podría ir en una clase base
-        pkl_file = file(self._pickle_filename(name), 'wb')
-        pickle.dump(var, pkl_file, 2)
-        pkl_file.close()
-
-    def _load_var(self, name):
-        r"_load_var() -> object :: Load a especific pickle file."
-        # XXX podría ir en una clase base
-        return pickle.load(file(self._pickle_filename(name)))
-
-    def _write_config(self):
-        r"_write_config() -> None :: Generate all the configuration files."
-        out_file = file(path.join(self.config_dir, config_filename), 'w')
-        ctx = Context(out_file,
-                    ip = self.vars['ip'],
-                    port = self.vars['port'],
-                    hosts = self.hosts.values()
-                    )
-        self.config_template.render_context(ctx)
-        out_file.close()
-
 
 if __name__ == '__main__':
 
diff --git a/services/util.py b/services/util.py
new file mode 100644 (file)
index 0000000..cbfc091
--- /dev/null
@@ -0,0 +1,558 @@
+# vim: set encoding=utf-8 et sw=4 sts=4 :
+
+import subprocess
+from mako.template import Template
+from mako.runtime import Context
+from os import path
+try:
+    import cPickle as pickle
+except ImportError:
+    import pickle
+
+from dispatcher import Handler, handler, HandlerError
+
+#DEBUG = False
+DEBUG = True
+
+__ALL__ = ('ServiceHandler', 'InitdHandler', 'Persistent', 'ConfigWriter',
+            'Error', 'ReturnNot0Error', 'ExecutionError', 'call')
+
+class Error(HandlerError):
+    r"""
+    Error(message) -> Error instance :: Base ServiceHandler exception class.
+
+    All exceptions raised by the ServiceHandler inherits from this one, so
+    you can easily catch any ServiceHandler exception.
+
+    message - A descriptive error message.
+    """
+
+    def __init__(self, message):
+        r"Initialize the object. See class documentation for more info."
+        self.message = message
+
+    def __str__(self):
+        return self.message
+
+class ReturnNot0Error(Error):
+    r"""
+    ReturnNot0Error(return_value) -> ReturnNot0Error instance.
+
+    A command didn't returned the expected 0 return value.
+
+    return_value - Return value returned by the command.
+    """
+
+    def __init__(self, return_value):
+        r"Initialize the object. See class documentation for more info."
+        self.return_value = return_value
+
+    def __str__(self):
+        return 'The command returned %d' % self.return_value
+
+class ExecutionError(Error):
+    r"""
+    ExecutionError(command, error) -> ExecutionError instance.
+
+    Error executing a command.
+
+    command - Command that was tried to execute.
+
+    error - Error received when trying to execute the command.
+    """
+
+    def __init__(self, command, error):
+        r"Initialize the object. See class documentation for more info."
+        self.command = command
+        self.error = error
+
+    def __str__(self):
+        command = self.command
+        if not isinstance(self.command, basestring):
+            command = ' '.join(command)
+        return "Can't execute command %s: %s" % (command, self.error)
+
+def call(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE, close_fds=True, universal_newlines=True,
+            **kw):
+    if DEBUG:
+        if not isinstance(command, basestring):
+            command = ' '.join(command)
+        print 'Executing command:', command
+        return
+    try:
+        r = subprocess.call(command, stdin=stdin, stdout=stdout, stderr=stderr,
+                                universal_newlines=universal_newlines,
+                                close_fds=close_fds, **kw)
+    except Exception, e:
+        raise ExecutionError(command, e)
+    if r is not 0:
+        raise ExecutionError(command, ReturnNot0Error(r))
+
+class ServiceHandler(Handler):
+    r"""ServiceHandler([start[, stop[, restart[, reload]]]]) -> ServiceHandler.
+
+    This is a helper class to inherit from to automatically handle services
+    with start, stop, restart, reload actions.
+
+    The actions can be defined by calling the constructor with all the
+    parameters or in a more declarative way as class attributes, like:
+
+    class TestHandler(ServiceHandler):
+        _service_start = ('command', 'start')
+        _service_stop = ('command', 'stop')
+        _service_restart = ('command', 'restart')
+        _service_reload = 'reload-command'
+
+    Commands are executed without using the shell, that's why they are specified
+    as tuples (where the first element is the command and the others are the
+    command arguments). If only a command is needed (without arguments) a single
+    string can be specified.
+
+    All commands must be specified.
+    """
+    # TODO implement it using metaclasses to add the handlers method by demand
+    # (only for specifieds commands).
+
+    def __init__(self, start=None, stop=None, restart=None, reload=None):
+        r"Initialize the object, see the class documentation for details."
+        for (name, action) in dict(start=start, stop=stop, restart=restart,
+                                                    reload=reload).items():
+            if action is not None:
+                setattr(self, '_service_%s' % name, action)
+
+    @handler(u'Start the service.')
+    def start(self):
+        r"start() -> None :: Start the service."
+        call(self._service_start)
+
+    @handler(u'Stop the service.')
+    def stop(self):
+        r"stop() -> None :: Stop the service."
+        call(self._service_stop)
+
+    @handler(u'Restart the service.')
+    def restart(self):
+        r"restart() -> None :: Restart the service."
+        call(self._service_restart)
+
+    @handler(u'Reload the service config (without restarting, if possible).')
+    def reload(self):
+        r"reload() -> None :: Reload the configuration of the service."
+        call(self._service_reload)
+
+class InitdHandler(Handler):
+    r"""InitdHandler([initd_name[, initd_dir]]) -> InitdHandler.
+
+    This is a helper class to inherit from to automatically handle services
+    with start, stop, restart, reload actions using a /etc/init.d like script.
+
+    The name and directory of the script can be defined by calling the
+    constructor or in a more declarative way as class attributes, like:
+
+    class TestHandler(ServiceHandler):
+        _initd_name = 'some-service'
+        _initd_dir = '/usr/local/etc/init.d'
+
+    The default _initd_dir is '/etc/init.d', _initd_name has no default and
+    must be specified in either way.
+
+    Commands are executed without using the shell.
+    """
+    # TODO implement it using metaclasses to add the handlers method by demand
+    # (only for specifieds commands).
+
+    _initd_dir = '/etc/init.d'
+
+    def __init__(self, initd_name=None, initd_dir=None):
+        r"Initialize the object, see the class documentation for details."
+        if initd_name is not None:
+            self._initd_name = initd_name
+        if initd_dir is not None:
+            self._initd_dir = initd_dir
+
+    @handler(u'Start the service.')
+    def start(self):
+        r"start() -> None :: Start the service."
+        call((path.join(self._initd_dir, self._initd_name), 'start'))
+
+    @handler(u'Stop the service.')
+    def stop(self):
+        r"stop() -> None :: Stop the service."
+        call((path.join(self._initd_dir, self._initd_name), 'stop'))
+
+    @handler(u'Restart the service.')
+    def restart(self):
+        r"restart() -> None :: Restart the service."
+        call((path.join(self._initd_dir, self._initd_name), 'restart'))
+
+    @handler(u'Reload the service config (without restarting, if possible).')
+    def reload(self):
+        r"reload() -> None :: Reload the configuration of the service."
+        call((path.join(self._initd_dir, self._initd_name), 'reload'))
+
+class Persistent:
+    r"""Persistent([vars[, dir[, ext]]]) -> Persistent.
+
+    This is a helper class to inherit from to automatically handle data
+    persistence using pickle.
+
+    The variables attributes to persist (vars), and the pickle directory (dir)
+    and file extension (ext) can be defined by calling the constructor or in a
+    more declarative way as class attributes, like:
+
+    class TestHandler(Persistent):
+        _persistent_vars = ('some_var', 'other_var')
+        _persistent_dir = 'persistent-data'
+        _persistent_ext = '.pickle'
+
+    The default dir is '.' and the default extension is '.pkl'. There are no
+    default variables, and they should be specified as string if a single
+    attribute should be persistent or as a tuple of strings if they are more.
+    The strings should be the attribute names to be persisted. For each
+    attribute a separated pickle file is generated in the pickle directory.
+
+    You can call _dump() and _load() to write and read the data respectively.
+    """
+    # TODO implement it using metaclasses to add the handlers method by demand
+    # (only for specifieds commands).
+
+    _persistent_vars = ()
+    _persistent_dir = '.'
+    _persistent_ext = '.pkl'
+
+    def __init__(self, vars=None, dir=None, ext=None):
+        r"Initialize the object, see the class documentation for details."
+        if vars is not None:
+            self._persistent_vars = vars
+        if dir is not None:
+            self._persistent_dir = dir
+        if ext is not None:
+            self._persistent_ext = ext
+
+    def _dump(self):
+        r"_dump() -> None :: Dump all persistent data to pickle files."
+        if isinstance(self._persistent_vars, basestring):
+            self._persistent_vars = (self._persistent_vars,)
+        for varname in self._persistent_vars:
+            self._dump_var(varname)
+
+    def _load(self):
+        r"_load() -> None :: Load all persistent data from pickle files."
+        if isinstance(self._persistent_vars, basestring):
+            self._persistent_vars = (self._persistent_vars,)
+        for varname in self._persistent_vars:
+            self._load_var(varname)
+
+    def _dump_var(self, varname):
+        r"_dump_var() -> None :: Dump a especific variable to a pickle file."
+        f = file(self._pickle_filename(varname), 'wb')
+        pickle.dump(getattr(self, varname), f, 2)
+        f.close()
+
+    def _load_var(self, varname):
+        r"_load_var() -> object :: Load a especific pickle file."
+        f = file(self._pickle_filename(varname))
+        setattr(self, varname, pickle.load(f))
+        f.close()
+
+    def _pickle_filename(self, name):
+        r"_pickle_filename() -> string :: Construct a pickle filename."
+        return path.join(self._persistent_dir, name) + self._persistent_ext
+
+class Restorable(Persistent):
+    r"""Restorable([defaults]) -> Restorable.
+
+    This is a helper class to inherit from that provides a nice _restore()
+    method to restore the persistent data if any, or load some nice defaults
+    if not.
+
+    The defaults can be defined by calling the constructor or in a more
+    declarative way as class attributes, like:
+
+    class TestHandler(Restorable):
+        _persistent_vars = ('some_var', 'other_var')
+        _restorable_defaults = dict(
+                some_var = 'some_default',
+                other_var = 'other_default')
+
+    The defaults is a dictionary, very coupled with the _persistent_vars
+    attribute inherited from Persistent. The defaults keys should be the
+    values from _persistent_vars, and the values the default values.
+
+    The _restore() method returns True if the data was restored successfully
+    or False if the defaults were loaded (in case you want to take further
+    actions). If a _write_config method if found, it's executed when a restore
+    fails too.
+    """
+    # TODO implement it using metaclasses to add the handlers method by demand
+    # (only for specifieds commands).
+
+    _restorable_defaults = dict()
+
+    def __init__(self, defaults=None):
+        r"Initialize the object, see the class documentation for details."
+        if defaults is not None:
+            self._restorable_defaults = defaults
+
+    def _restore(self):
+        r"_restore() -> bool :: Restore persistent data or create a default."
+        try:
+            self._load()
+            return True
+        except IOError:
+            for (k, v) in self._restorable_defaults.items():
+                setattr(self, k, v)
+            self._dump()
+            if hasattr(self, '_write_config'):
+                self._write_config()
+            return False
+
+class ConfigWriter:
+    r"""ConfigWriter([initd_name[, initd_dir]]) -> ConfigWriter.
+
+    This is a helper class to inherit from to automatically handle
+    configuration generation. Mako template system is used for configuration
+    files generation.
+
+    The configuration filenames, the generated configuration files directory
+    and the templates directory can be defined by calling the constructor or
+    in a more declarative way as class attributes, like:
+
+    class TestHandler(ConfigWriter):
+        _config_writer_files = ('base.conf', 'custom.conf')
+        _config_writer_cfg_dir = '/etc/service'
+        _config_writer_tpl_dir = 'templates'
+
+    The generated configuration files directory defaults to '.' and the
+    templates directory to 'templates'. _config_writer_files has no default and
+    must be specified in either way. It can be string or a tuple if more than
+    one configuration file must be generated.
+
+    The template filename and the generated configuration filename are both the
+    same (so if you want to generate some /etc/config, you should have some
+    templates/config template). That's why _config_writer_cfg_dir and
+    _config_writer_tpl_dir can't be the same.
+
+    When you write your Handler, you should call _config_build_templates() in
+    you Handler constructor to build the templates.
+
+    To write the configuration files, you must use the _write_config() method.
+    To know what variables to replace in the template, you have to provide a
+    method called _get_config_vars(tamplate_name), which should return a
+    dictionary of variables to pass to the template system to be replaced in
+    the template for the configuration file 'config_file'.
+    """
+    # TODO implement it using metaclasses to add the handlers method by demand
+    # (only for specifieds commands).
+
+    _config_writer_files = ()
+    _config_writer_cfg_dir = '.'
+    _config_writer_tpl_dir = 'templates'
+
+    def __init__(self, files=None, cfg_dir=None, tpl_dir=None):
+        r"Initialize the object, see the class documentation for details."
+        if files is not None:
+            self._config_writer_files = files
+        if cfg_dir is not None:
+            self._config_writer_cfg_dir = cfg_dir
+        if tpl_dir is not None:
+            self._config_writer_tpl_dir = tpl_dir
+        self._config_build_templates()
+
+    def _config_build_templates(self):
+        r"_config_writer_templates() -> None :: Build the template objects."
+        if isinstance(self._config_writer_files, basestring):
+            self._config_writer_files = (self._config_writer_files,)
+        if not hasattr(self, '_config_writer_templates') \
+                                        or not self._config_writer_templates:
+            self._config_writer_templates = dict()
+            for t in self._config_writer_files:
+                f = path.join(self._config_writer_tpl_dir, t)
+                self._config_writer_templates[t] = Template(filename=f)
+
+    def _render_config(self, template_name, vars=None):
+        r"""_render_config(template_name[, config_filename[, vars]]).
+
+        Render a single config file using the template 'template_name'. If
+        vars is specified, it's used as the dictionary with the variables
+        to replace in the templates, if not, it looks for a
+        _get_config_vars() method to get it.
+        """
+        if vars is None:
+            if hasattr(self, '_get_config_vars'):
+                vars = self._get_config_vars(template_name)
+            else:
+                vars = dict()
+        elif callable(vars):
+            vars = vars(template_name)
+        return self._config_writer_templates[template_name].render(**vars)
+
+    def _write_single_config(self, template_name, config_filename=None, vars=None):
+        r"""_write_single_config(template_name[, config_filename[, vars]]).
+
+        Write a single config file using the template 'template_name'. If no
+        config_filename is specified, the config filename will be the same as
+        the 'template_name' (but stored in the generated config files
+        directory). If it's specified, the generated config file is stored in
+        the file called 'config_filename' (also in the generated files
+        directory). If vars is specified, it's used as the dictionary with the
+        variables to replace in the templates, if not, it looks for a
+        _get_config_vars() method to get it.
+        """
+        if not config_filename:
+            config_filename = template_name
+        if vars is None:
+            if hasattr(self, '_get_config_vars'):
+                vars = self._get_config_vars(template_name)
+            else:
+                vars = dict()
+        elif callable(vars):
+            vars = vars(template_name)
+        f = file(path.join(self._config_writer_cfg_dir, config_filename), 'w')
+        ctx = Context(f, **vars)
+        self._config_writer_templates[template_name].render_context(ctx)
+        f.close()
+
+    def _write_config(self):
+        r"_write_config() -> None :: Generate all the configuration files."
+        for t in self._config_writer_files:
+            self._write_single_config(t)
+
+class TransactionalHandler(Handler):
+    r"""TransactionalHandler([initd_name[, initd_dir]]) -> TransactionalHandler.
+
+    This is a helper class to inherit from to automatically handle
+    transactional handlers, which have commit and rollback commands.
+
+    The handler should provide a reload() method (see ServiceHandler and
+    InitdHandler for helper classes to provide this) which will be called
+    when a commit command is issued (if a reload() command is present).
+    The persistent data will be written too (if a _dump() method is provided,
+    see Persistent and Restorable for that), and the configuration files
+    will be generated (if a _write_config method is present, see ConfigWriter).
+    """
+    # TODO implement it using metaclasses to add the handlers method by demand
+    # (only for specifieds commands).
+
+    @handler(u'Commit the changes (reloading the service, if necessary).')
+    def commit(self):
+        r"commit() -> None :: Commit the changes and reload the service."
+        if hasattr(self, '_dump'):
+            self._dump()
+        if hasattr(self, '_write_config'):
+            self._write_config()
+        if hasattr(self, '_reload'):
+            self.reload()
+
+    @handler(u'Discard all the uncommited changes.')
+    def rollback(self):
+        r"rollback() -> None :: Discard the changes not yet commited."
+        if hasattr(self, '_load'):
+            self._load()
+
+
+if __name__ == '__main__':
+
+    # Execution tests
+    class STestHandler1(ServiceHandler):
+        _service_start = ('service', 'start')
+        _service_stop = ('service', 'stop')
+        _service_restart = ('ls', '/')
+        _service_reload = ('cp', '/la')
+    class STestHandler2(ServiceHandler):
+        def __init__(self):
+            ServiceHandler.__init__(self, 'cmd-start', 'cmd-stop',
+                                        'cmd-restart', 'cmd-reload')
+    class ITestHandler1(InitdHandler):
+        _initd_name = 'test1'
+    class ITestHandler2(InitdHandler):
+        def __init__(self):
+            InitdHandler.__init__(self, 'test2', '/usr/local/etc/init.d')
+    handlers = [
+        STestHandler1(),
+        STestHandler2(),
+        ITestHandler1(),
+        ITestHandler2(),
+    ]
+    for h in handlers:
+        print h.__class__.__name__
+        try:
+            h.start()
+        except ExecutionError, e:
+            print e
+        try:
+            h.stop()
+        except ExecutionError, e:
+            print e
+        try:
+            h.restart()
+        except ExecutionError, e:
+            print e
+        try:
+            h.reload()
+        except ExecutionError, e:
+            print e
+        print
+
+    # Persistent test
+    print 'PTestHandler'
+    class PTestHandler(Persistent):
+        _persistent_vars = 'vars'
+        def __init__(self):
+            self.vars = dict(a=1, b=2)
+    h = PTestHandler()
+    print h.vars
+    h._dump()
+    h.vars['x'] = 100
+    print h.vars
+    h._load()
+    print h.vars
+    h.vars['x'] = 100
+    h._dump()
+    print h.vars
+    del h.vars['x']
+    print h.vars
+    h._load()
+    print h.vars
+    print
+
+    # Restorable test
+    print 'RTestHandler'
+    class RTestHandler(Restorable):
+        _persistent_vars = 'vars'
+        _restorable_defaults = dict(vars=dict(a=1, b=2))
+        def __init__(self):
+            self._restore()
+    h = RTestHandler()
+    print h.vars
+    h.vars['x'] = 100
+    h._dump()
+    h = RTestHandler()
+    print h.vars
+    print
+
+    # ConfigWriter test
+    print 'CTestHandler'
+    import os
+    os.mkdir('templates')
+    f = file('templates/config', 'w')
+    f.write('Hello, ${name}! You are ${what}.')
+    f.close()
+    print 'template:'
+    print file('templates/config').read()
+    class CTestHandler(ConfigWriter):
+        _config_writer_files = 'config'
+        def __init__(self):
+            self._config_build_templates()
+        def _get_config_vars(self, config_file):
+            return dict(name='you', what='a parrot')
+    h = CTestHandler()
+    h._write_config()
+    print 'config:'
+    print file('config').read()
+    os.unlink('config')
+    os.unlink('templates/config')
+    os.rmdir('templates')
+    print
+