]> git.llucax.com Git - software/pymin.git/commitdiff
Big repository (and source) rebump.
authorLeandro Lucarella <llucax@gmail.com>
Sat, 22 Sep 2007 04:50:53 +0000 (01:50 -0300)
committerLeandro Lucarella <llucax@gmail.com>
Sat, 22 Sep 2007 04:50:53 +0000 (01:50 -0300)
A lot was changed, is allmost a new repo import =P

14 files changed:
.gitignore [new file with mode: 0644]
README
TODO [new file with mode: 0644]
config.py [new file with mode: 0644]
dispatcher.py
doc/config/dhcp/dhcpd.conf [moved from config/dhcp/dhcpd.conf with 100% similarity]
doc/config/dns/%%(zone1).zone [moved from config/dns/%%(zone1).zone with 100% similarity]
doc/config/dns/named.conf [moved from config/dns/named.conf with 100% similarity]
eventloop.py [new file with mode: 0644]
pollserver.py [deleted file]
pymin [new file with mode: 0755]
pymindaemon.py [new file with mode: 0644]
services/__init__.py [new file with mode: 0644]
udp_server.py [deleted file]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..0d20b64
--- /dev/null
@@ -0,0 +1 @@
+*.pyc
diff --git a/README b/README
index 24abc5d4bfbefe1d46eaf2bb9a28f0f6599bf423..1912cbbf5ca5b2d72ba4b5e02f50f4dc4af3ce36 100644 (file)
--- 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 (file)
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 (file)
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
+)
+
index 78ae2460d4c7b1c6e2074a26e3148ba7d864f864..2f545cd6e91fb7351c65b223092b253863b5a0a6 100644 (file)
 # 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/eventloop.py b/eventloop.py
new file mode 100644 (file)
index 0000000..7da492d
--- /dev/null
@@ -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 (file)
index 886ef3a..0000000
+++ /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 (executable)
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 (file)
index 0000000..1868ae2
--- /dev/null
@@ -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 (file)
index 0000000..8b13789
--- /dev/null
@@ -0,0 +1 @@
+
diff --git a/udp_server.py b/udp_server.py
deleted file mode 100644 (file)
index 23248a8..0000000
+++ /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