From: Leandro Lucarella Date: Sat, 22 Sep 2007 04:50:53 +0000 (-0300) Subject: Big repository (and source) rebump. X-Git-Url: https://git.llucax.com/software/pymin.git/commitdiff_plain/026f5f3a6274709b7863c619c979dd32f0ee686f?ds=sidebyside Big repository (and source) rebump. A lot was changed, is allmost a new repo import =P --- diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0d20b64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/README b/README index 24abc5d..1912cbb 100644 --- a/README +++ b/README @@ -18,4 +18,4 @@ de errores, pero es fácil ponerlo. Para hacer una prueba simple del dispatcher: python dispatcher.py -http://www.cisco.com/en/US/products/sw/iosswrel/ps5187/prod_command_reference_list.html \ No newline at end of file +http://www.cisco.com/en/US/products/sw/iosswrel/ps5187/prod_command_reference_list.html diff --git a/TODO b/TODO new file mode 100644 index 0000000..c7427e3 --- /dev/null +++ b/TODO @@ -0,0 +1,17 @@ + +Ideas / TODO: + +* Soportar comillas para argumentos con espacios y otros caracteres, onda: + 'misc set motd "Hola!\nEste es el servidor de garombia"' + +* Soportar keyword arguments, onda que: + 'dns set pepe=10.10.10.1 juan=10.10.10.2' + se mapee a algo como: dns.set(pepe='10.10.10.1', juan='10.10.10.2') + +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. + +Por otro lado, el cliente de consola, por que no es el cliente web pero +accedido via ssh usando un navegador de texto como w3m??? + diff --git a/config.py b/config.py new file mode 100644 index 0000000..93a6b7b --- /dev/null +++ b/config.py @@ -0,0 +1,17 @@ +# vim: set et sts=4 sw=4 encoding=utf-8 : + +# XXX for testing only +def test_func(*args): + print 'func:', args + +routes = dict \ +( + test = test_func, +) + +bind_addr = \ +( + '', # Bind IP ('' is ANY) + 9999, # Port +) + diff --git a/dispatcher.py b/dispatcher.py index 78ae246..2f545cd 100644 --- a/dispatcher.py +++ b/dispatcher.py @@ -1,66 +1,117 @@ # vim: set et sts=4 sw=4 encoding=utf-8 : -class BadRouteError(Exception): +r""" +Command dispatcher. - def __init__(self,cmd): - self.cmd = cmd +This module provides a convenient and extensible command dispatching mechanism. +It's based on Zope or Cherrypy dispatching (but implemented from the scratch) +and translates commands to functions/objects/methods. +""" - def __str__(self): - return repr(cmd) +class Error(RuntimeError): + r""" + Error(command) -> Error instance :: Base dispatching exceptions class. -class CommandNotFoundError(Exception): + All exceptions raised by the Dispatcher inherits from this one, so you can + easily catch any dispatching exception. - def __init__(self,cmd): - self.cmd = cmd + command - is the command that raised the exception. + """ - def __str__(self): - return repr(cmd) + def __init__(self, command): + r"""Initialize the Error object. -class Dispatcher: - - def __init__(self, routes=dict()): - self.routes = routes - - def dispatch(self, route): - route = route.split() # TODO considerar comillas - try: - handler = self.routes[route[0]] - route = route[1:] - while not callable(handler): - handler = getattr(handler, route[0]) - route = route[1:] - handler(*route) - - except KeyError: - raise CommandNotFoundError(route[0]) - except AttributeError: - raise BadRouteError(route[0]) - except IndexError: - pass - - -def test_func(*args): - print 'func:', args + See Error class documentation for more info. + """ + self.command = command + def __str__(self): + return repr(self.command) -class TestClassSubHandler: +class CommandNotFoundError(Error): + r""" + CommandNotFoundError(command) -> CommandNotFoundError instance - def subcmd(self, *args): - print 'class.subclass.subcmd:', args + This exception is raised when the command received can't be dispatched + because there is no handlers to process it. + """ + pass +class Dispatcher: + r"""Dispatcher([routes]) -> Dispatcher instance :: Command dispatcher + + This class provides a modular and extensible dispatching mechanism. You + can specify root 'routes' (as a dict where the key is the string of the + root command and the value is a callable object to handle that command, + or a subcommand if the callable is an instance and the command can be + sub-routed). + + The command can have arguments, separated by (any number of) spaces. + + The dispatcher tries to route the command as deeply as it can, passing + the other "path" components as arguments to the callable. To route the + command it inspects the callable attributes to find a suitable callable + attribute to handle the command in a more specific way, and so on. + + Example: + >>> d = Dispatcher(dict(handler=some_handler)) + >>> d.dispatch('handler attribute method arg1 arg2') + + If 'some_handler' is an object with an 'attribute' that is another + object which has a method named 'method', then + some_handler.attribute.method('arg1', 'arg2') will be called. If + some_handler is a function, then some_handler('attribute', 'method', + 'arg1', 'arg2') will be called. The handler "tree" can be as complex + and deep as you want. + + If some command can't be dispatched (because there is no root handler or + there is no matching callable attribute), a CommandNotFoundError is raised. + """ + + def __init__(self, routes=dict()): + r"""Initialize the Dispatcher object. + + See Dispatcher class documentation for more info. + """ + self.routes = routes + + def dispatch(self, route): + r"""dispatch(route) -> None :: Dispatch a command string. + + This method searches for a suitable callable object in the routes + "tree" and call it, or raises a CommandNotFoundError if the command + can't be dispatched. + """ + route = route.split() # TODO support "" and keyword arguments + if not route: + raise CommandNotFoundError('') # TODO better error reporting + handler = self.routes.get(route[0], None) + route = route[1:] + while not callable(handler): + if not route: + raise CommandNotFoundError('XXX') # TODO better error reporting + if not hasattr(handler, route[0]): + raise CommandNotFoundError(route[0]) # TODO better error rep. + handler = getattr(handler, route[0]) + route = route[1:] + handler(*route) -class TestClass: - def cmd1(self, *args): - print 'class.cmd1:', args +if __name__ == '__main__': - def cmd2(self, *args): - print 'class.cmd2:', args + def test_func(*args): + print 'func:', args - subclass = TestClassSubHandler() + class TestClassSubHandler: + def subcmd(self, *args): + print 'class.subclass.subcmd:', args - -if __name__ == '__main__': + class TestClass: + def cmd1(self, *args): + print 'class.cmd1:', args + def cmd2(self, *args): + print 'class.cmd2:', args + subclass = TestClassSubHandler() d = Dispatcher(dict( func=test_func, @@ -70,20 +121,16 @@ if __name__ == '__main__': d.dispatch('func arg1 arg2 arg3') d.dispatch('inst cmd1 arg1 arg2 arg3 arg4') d.dispatch('inst subclass subcmd arg1 arg2 arg3 arg4 arg5') - -# Ideas / TODO: -# -# * Soportar comillas para argumentos con espacios y otros caracteres, onda: -# 'misc set motd "Hola!\nEste es el servidor de garombia"' -# -# * Soportar keyword arguments, onda que: -# 'dns set pepe=10.10.10.1 juan=10.10.10.2' -# se mapee a algo como: dns.set(pepe='10.10.10.1', juan='10.10.10.2') -# -# 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. -# -# Por otro lado, el cliente de consola, por que no es el cliente web pero -# accedido via ssh usando un navegador de texto como w3m??? + try: + d.dispatch('') + except CommandNotFoundError, e: + print 'Not found:', e + try: + d.dispatch('sucutrule') + except CommandNotFoundError, e: + print 'Not found:', e + try: + d.dispatch('inst cmd3') + except CommandNotFoundError, e: + print 'Not found:', e diff --git a/config/dhcp/dhcpd.conf b/doc/config/dhcp/dhcpd.conf similarity index 100% rename from config/dhcp/dhcpd.conf rename to doc/config/dhcp/dhcpd.conf diff --git a/config/dns/%%(zone1).zone b/doc/config/dns/%%(zone1).zone similarity index 100% rename from config/dns/%%(zone1).zone rename to doc/config/dns/%%(zone1).zone diff --git a/config/dns/named.conf b/doc/config/dns/named.conf similarity index 100% rename from config/dns/named.conf rename to doc/config/dns/named.conf diff --git a/eventloop.py b/eventloop.py new file mode 100644 index 0000000..7da492d --- /dev/null +++ b/eventloop.py @@ -0,0 +1,144 @@ +# vim: set encoding=utf-8 et sw=4 sts=4 : + +r""" +A simple event loop. + +Please see EventLoop class documentation for more info. +""" + +from select import poll, POLLIN, POLLPRI, POLLERR + +__ALL__ = ('EventLoop') + +class EventLoop: + r"""EventLoop(file[, handler]) -> EventLoop instance + + This class implements a simple event loop based on select module. + It "listens" to activity a single 'file' object (a file, a pipe, + a socket, or even a simple file descriptor) and calls a 'handler' + function (or the handle() method if you prefer subclassing) every + time the file is ready for reading (or has an error). + + This is a really simple example of usage using a hanlder callable: + + >>> import os + >>> def handle(event_loop): + data = os.read(event_loop.fileno, 100) + os.write(1, 'Received message: %r\n' % data) + >>> p = EventLoop(0, handle) + >>> p.loop(once=True) + + In this example only one event is handled (see the 'once' argument + of loop). + + A more complex example, making a subclass and explicitly stopping + the loop, looks something like this: + + >>> class Test(EventLoop): + >>> def handle(self): + >>> data = os.read(self.fileno, 100) + >>> if data == 'q\n': + >>> self.stop() + >>> else: + >>> os.write(1, 'Received message: %r\n' % data) + >>> p = Test(0) + >>> p.loop() + + This example loops until the user enters a single "q", when stop() + is called and the event loop is exited. + """ + + def __init__(self, file, handler=None): + r"""Initialize the EventLoop object. + + See EventLoop class documentation for more info. + """ + self.poll = poll() + self._stop = False + self.__register(file) + self.handler = handler + + def __register(self, file): + r"__register(file) -> None :: Register a new file for polling." + self._file = file + self.poll.register(self.fileno, POLLIN | POLLPRI | POLLERR) + + def set_file(self, file): + r"""set_file(file) -> None :: New file object to be monitored + + Unregister the previous file object being monitored and register + a new one. + """ + self.poll.unregister(self.fileno) + self.__register(file) + + def get_file(self): + r"get_file() -> file object/int :: Get the current file object/fd." + return self._file + + file = property(get_file, set_file, doc='File object (or descriptor)') + + def get_fileno(self): + r"get_fileno() -> int :: Get the current file descriptor" + if hasattr(self.file, 'fileno'): + return self.file.fileno() + return self.file + + fileno = property(get_fileno, doc='File descriptor (never a file object)') + + def stop(self): + r"""stop() -> None :: Stop the event loop. + + The event loop will be interrupted as soon as the current handler + finishes. + """ + self._stop = True + + def loop(self, once=False): + r"""loop([once]) -> None :: Wait for events. + + Wait for events and handle then when they arrive. If once is True, + then only 1 event is processed and then this method returns. + """ + while True: + res = self.poll.poll() + if self.handler is not None: + self.handler(self) + else: + self.handle() + if self._stop or once: + self._stop = False + break + + def handle(self): + r"handle() -> None :: Abstract method to be overriden to handle events." + raise NotImplementedError + +if __name__ == '__main__': + + import os + + def handle(event_loop): + data = os.read(event_loop.fileno, 100) + os.write(1, 'Received message: %r\n' % data) + + p = EventLoop(0, handle) + + os.write(1, 'Say something once: ') + p.loop(once=True) + os.write(1, 'Great!\n') + + class Test(EventLoop): + def handle(self): + data = os.read(self.fileno, 100) + if data == 'q\n': + self.stop() + else: + os.write(1, 'Received message: %r\n' % data) + + p = Test(0) + + os.write(1, 'Say a lot of things, then press write just "q" to stop: ') + p.loop() + os.write(1, 'Ok, bye!\n') + diff --git a/pollserver.py b/pollserver.py deleted file mode 100644 index 886ef3a..0000000 --- a/pollserver.py +++ /dev/null @@ -1,39 +0,0 @@ -# vim: set encoding=utf-8 et sw=4 sts=4 : - -import signal -import select -from sys import exit - -import dispatcher as dis -import udp_server as us - -def quit(signum, frame): - print "Shuting down ..." - exit(0) - -signal.signal(signal.SIGINT, quit) -signal.signal(signal.SIGTERM, quit) - -server = us.UDPServer(9999) - -poll = select.poll() -poll.register(server.sock.fileno(), select.POLLIN | select.POLLPRI) - -d = dis.Dispatcher(dict( - func=dis.test_func, - inst=dis.TestClass() - )) - -def handle_recv(sock): - (msg, addr) = sock.recvfrom(65535) - try: - d.dispatch(msg) - except dis.BadRouteError, inst: - sock.sendto('Bad route from : ' + inst.cmd + '\n', addr) - except dis.CommandNotFoundError, inst: - sock.sendto('Command not found : ' + inst.cmd + '\n', addr) - -while True: - l = poll.poll() - handle_recv(server.sock) - diff --git a/pymin b/pymin new file mode 100755 index 0000000..829b9a5 --- /dev/null +++ b/pymin @@ -0,0 +1,8 @@ +#!/usr/bin/env python +# vim: set encoding=utf-8 et sw=4 sts=4 : + +from pymindaemon import PyminDaemon +import config + +PyminDaemon(config.bind_addr, config.routes).run() + diff --git a/pymindaemon.py b/pymindaemon.py new file mode 100644 index 0000000..1868ae2 --- /dev/null +++ b/pymindaemon.py @@ -0,0 +1,79 @@ +# vim: set encoding=utf-8 et sw=4 sts=4 : + +r""" +Python Administration Daemon. + +Python Administration Daemon is an modular, extensible administration tool +to administrate a set of services remotely (or localy) throw a simple +command-line. +""" + +import signal +import socket +from dispatcher import Dispatcher +from eventloop import EventLoop + +class PyminDaemon(EventLoop): + r"""PyminDaemon(bind_addr, routes) -> PyminDaemon instance + + This class is well suited to run as a single process. It handles + signals for controlled termination (SIGINT and SIGTERM), as well as + a user signal to reload the configuration files (SIGUSR1). + + bind_addr - is a tuple of (ip, port) where to bind the UDP socket to. + + routes - is a dictionary where the key is a command string and the value + is the command handler. This is passed directly to the Dispatcher. + + Here is a simple usage example: + + >>> def test_handler(*args): print 'test:', args + >>> PyminDaemon(('', 9999), dict(test=test_handler)).run() + """ + + def __init__(self, bind_addr, routes): + r"""Initialize the PyminDaemon object. + + See PyminDaemon class documentation for more info. + """ + # Create and bind socket + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind(bind_addr) + # Create EventLoop + EventLoop.__init__(self, sock) + # Create Dispatcher + self.dispatcher = Dispatcher(routes) + # Signal handling + def quit(signum, frame): + print "Shuting down ..." + loop.stop() # tell main event loop to stop + def reload_config(signum, frame): + print "Reloading configuration..." + # TODO iterate handlers list propagating reload action + signal.signal(signal.SIGINT, quit) + signal.signal(signal.SIGTERM, quit) + signal.signal(signal.SIGUSR1, reload_config) + + def handle(self): + r"handle() -> None :: Handle incoming events using the dispatcher." + (msg, addr) = self.file.recvfrom(65535) + self.dispatcher.dispatch(msg) + #try: + # d.dispatch(msg) + #except dis.BadRouteError, inst: + # sock.sendto('Bad route from : ' + inst.cmd + '\n', addr) + #except dis.CommandNotFoundError, inst: + # sock.sendto('Command not found : ' + inst.cmd + '\n', addr) + + def run(self): + r"run() -> None :: Run the event loop (shortcut to loop())" + return self.loop() + +if __name__ == '__main__': + + def test_handler(*args): + print 'test:', args + + PyminDaemon(('', 9999), dict(test=test_handler)).run() + diff --git a/services/__init__.py b/services/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/services/__init__.py @@ -0,0 +1 @@ + diff --git a/udp_server.py b/udp_server.py deleted file mode 100644 index 23248a8..0000000 --- a/udp_server.py +++ /dev/null @@ -1,11 +0,0 @@ -# vim: set encoding=utf-8 et sw=4 sts=4 : - -import socket as s - -class UDPServer: - "Udp server class" - - def __init__(self, port): - self.sock = s.socket(s.AF_INET, s.SOCK_DGRAM) - self.sock.setsockopt(s.SOL_SOCKET, s.SO_REUSEADDR, 1) - self.sock.bind(('', port)) \ No newline at end of file