From: Leandro Lucarella Date: Wed, 3 Oct 2007 21:41:48 +0000 (-0300) Subject: Factored out a lot of common code. X-Git-Url: https://git.llucax.com/software/pymin.git/commitdiff_plain/d6dfd46ed31d985e364b9ea9a404c394f15c2348?ds=sidebyside;hp=--cc Factored out a lot of common code. 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. --- d6dfd46ed31d985e364b9ea9a404c394f15c2348 diff --git a/TODO b/TODO index 18d2325..362b434 100644 --- 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. diff --git a/config.py b/config.py index f155fcb..a0f1c45 100644 --- 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 = \ diff --git a/services/__init__.py b/services/__init__.py index bd7c3c1..4d4dfb6 100644 --- a/services/__init__.py +++ b/services/__init__.py @@ -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 diff --git a/services/dhcp/__init__.py b/services/dhcp/__init__.py index 08bb8b5..7ffadcf 100644 --- a/services/dhcp/__init__.py +++ b/services/dhcp/__init__.py @@ -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)) diff --git a/services/dns/__init__.py b/services/dns/__init__.py index 8f08d35..e3739e3 100644 --- a/services/dns/__init__.py +++ b/services/dns/__init__.py @@ -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: diff --git a/services/firewall/__init__.py b/services/firewall/__init__.py index 1ac1e60..bbdb3fd 100644 --- a/services/firewall/__init__.py +++ b/services/firewall/__init__.py @@ -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') diff --git a/services/ip/__init__.py b/services/ip/__init__.py index bc8b275..cf16088 100644 --- a/services/ip/__init__.py +++ b/services/ip/__init__.py @@ -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__': diff --git a/services/ip/templates/device.command b/services/ip/templates/device similarity index 100% rename from services/ip/templates/device.command rename to services/ip/templates/device diff --git a/services/ip/templates/ip_add.command b/services/ip/templates/ip_add similarity index 100% rename from services/ip/templates/ip_add.command rename to services/ip/templates/ip_add diff --git a/services/ip/templates/ip_del.command b/services/ip/templates/ip_del similarity index 100% rename from services/ip/templates/ip_del.command rename to services/ip/templates/ip_del diff --git a/services/ip/templates/ip_flush.command b/services/ip/templates/ip_flush similarity index 100% rename from services/ip/templates/ip_flush.command rename to services/ip/templates/ip_flush diff --git a/services/ip/templates/route_add.command b/services/ip/templates/route_add similarity index 100% rename from services/ip/templates/route_add.command rename to services/ip/templates/route_add diff --git a/services/ip/templates/route_del.command b/services/ip/templates/route_del similarity index 100% rename from services/ip/templates/route_del.command rename to services/ip/templates/route_del diff --git a/services/ip/templates/route_flush.command b/services/ip/templates/route_flush similarity index 100% rename from services/ip/templates/route_flush.command rename to services/ip/templates/route_flush diff --git a/services/proxy/__init__.py b/services/proxy/__init__.py index 2873569..195125c 100644 --- a/services/proxy/__init__.py +++ b/services/proxy/__init__.py @@ -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 index 0000000..cbfc091 --- /dev/null +++ b/services/util.py @@ -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 +