]> git.llucax.com Git - software/pymin.git/blob - pymin/pymindaemon.py
Move services outside the "static" pymin modules structure (refs #27).
[software/pymin.git] / pymin / pymindaemon.py
1 # vim: set encoding=utf-8 et sw=4 sts=4 :
2
3 r"""
4 Python Administration Daemon.
5
6 Python Administration Daemon is an modular, extensible administration tool
7 to administrate a set of services remotely (or localy) throw a simple
8 command-line.
9 """
10
11 import signal
12 import socket
13 import formencode
14 import logging ; log = logging.getLogger('pymin.pymindaemon')
15
16 from pymin.dispatcher import handler
17 from pymin import dispatcher
18 from pymin import eventloop
19 from pymin import serializer
20 from pymin import procman
21
22 class PyminDaemon(eventloop.EventLoop):
23     r"""PyminDaemon(root, bind_addr) -> PyminDaemon instance
24
25     This class is well suited to run as a single process. It handles
26     signals for controlled termination (SIGINT and SIGTERM), as well as
27     a user signal to reload the configuration files (SIGUSR1).
28
29     root - the root handler. This is passed directly to the Dispatcher.
30
31     bind_addr - is a tuple of (ip, port) where to bind the UDP socket to.
32
33     Here is a simple usage example:
34
35     >>> from pymin import dispatcher
36     >>> class Root(dispatcher.Handler):
37             @handler('Test command.')
38             def test(self, *args):
39                 print 'test:', args
40     >>> PyminDaemon(Root(), ('', 9999)).run()
41
42     The daemon then will be listening to messages to UDP port 9999. Messages
43     will be dispatcher throgh the pymin.dispatcher mechanism. If all goes ok,
44     an OK response is sent. If there was a problem, an ERROR response is sent.
45
46     The general syntax of responses is::
47
48         (OK|ERROR) LENGTH
49         CSV MESSAGE
50
51     So, first is a response code (OK or ERROR), then it comes the length of
52     the CSV MESSAGE (the bufer needed to receive the rest of the message).
53
54     CSV MESSAGE is the body of the response, which it could be void (if lenght
55     is 0), a simple string (a CVS with only one column and row), a single row
56     or a full "table" (a CSV with multiple rows and columns).
57
58     There are 2 kind of errors considered "normal": dispatcher.Error and
59     formencode.Invalid. In general, response bodies of errors are simple
60     strings, except, for example, for formencode.Invalid errors where an
61     error_dict is provided. In that case the error is a "table", where the
62     first colunm is the name of an invalid argument, and the second is the
63     description of the error for that argument. Any other kind of exception
64     raised by the handlers will return an ERROR response with the description
65     "Internal server error".
66
67     All messages (requests and responses) should be UTF-8 encoded and the CVS
68     responses are formated in "Excel" format, as known by the csv module.
69     """
70
71     def __init__(self, root, bind_addr=('', 9999), timer=1):
72         r"""Initialize the PyminDaemon object.
73
74         See PyminDaemon class documentation for more info.
75         """
76         log.debug(u'PyminDaemon(%r, %r, %r)', root, bind_addr, timer)
77         # Timer timeout time
78         self.timer = timer
79         # Create and bind socket
80         sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
81         sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
82         sock.bind(bind_addr)
83         # Signal handling
84         def quit(loop, signum):
85             log.debug(u'PyminDaemon quit() handler: signal %r', signum)
86             log.info(u'Shutting down...')
87             loop.stop() # tell main event loop to stop
88         def reload_config(loop, signum):
89             log.debug(u'PyminDaemon reload_config() handler: signal %r', signum)
90             log.info(u'Reloading configuration...')
91             # TODO iterate handlers list propagating reload action
92         def timer(loop, signum):
93             loop.handle_timer()
94             signal.alarm(loop.timer)
95         def child(loop, signum):
96             procman.sigchild_handler(signum)
97         # Create EventLoop
98         eventloop.EventLoop.__init__(self, sock, signals={
99                 signal.SIGINT: quit,
100                 signal.SIGTERM: quit,
101                 signal.SIGUSR1: reload_config,
102                 signal.SIGALRM: timer,
103                 signal.SIGCHLD: child,
104             })
105         # Create Dispatcher
106         #TODO root.pymin = PyminHandler()
107         self.dispatcher = dispatcher.Dispatcher(root)
108
109     def handle(self):
110         r"handle() -> None :: Handle incoming events using the dispatcher."
111         (msg, addr) = self.file.recvfrom(65535)
112         log.debug(u'PyminDaemon.handle: message %r from %r', msg, addr)
113         response = u'ERROR'
114         try:
115             result = self.dispatcher.dispatch(unicode(msg, 'utf-8'))
116             if result is not None:
117                 result = serializer.serialize(result)
118             response = u'OK'
119         except dispatcher.Error, e:
120             result = unicode(e) + u'\n'
121         except formencode.Invalid, e:
122             if e.error_dict:
123                 result = serializer.serialize(e.error_dict)
124             else:
125                 result = unicode(e) + u'\n'
126         except Exception, e:
127             import traceback
128             result = u'Internal server error\n'
129             log.exception(u'PyminDaemon.handle: unhandled exception')
130         if result is None:
131             response += u' 0\n'
132         else:
133             response += u' %d\n%s' % (len(result), result)
134         log.debug(u'PyminDaemon.handle: response %r to %r', response, addr)
135         self.file.sendto(response.encode('utf-8'), addr)
136
137     def handle_timer(self):
138         r"handle_timer() -> None :: Call handle_timer() on handlers."
139         self.dispatcher.root.handle_timer()
140
141     def run(self):
142         r"run() -> None :: Run the event loop (shortcut to loop())"
143         log.debug(u'PyminDaemon.loop()')
144         # Start the timer
145         self.handle_timer()
146         signal.alarm(self.timer)
147         # Loop
148         try:
149             return self.loop()
150         except eventloop.LoopInterruptedError, e:
151             log.debug(u'PyminDaemon.loop: interrupted')
152             pass
153
154 if __name__ == '__main__':
155
156     logging.basicConfig(
157         level   = logging.DEBUG,
158         format  = '%(asctime)s %(levelname)-8s %(message)s',
159         datefmt = '%H:%M:%S',
160     )
161
162     class Scheme(formencode.Schema):
163         mod = formencode.validators.OneOf(['upper', 'lower'], if_empty='lower')
164         ip = formencode.validators.CIDR
165
166     class Root(dispatcher.Handler):
167         @handler(u"Print all the arguments, return nothing.")
168         def test(self, *args):
169             print 'test:', args
170         @handler(u"Echo the message passed as argument.")
171         def echo(self, message, mod=None, ip=None):
172             vals = Scheme.to_python(dict(mod=mod, ip=ip))
173             mod = vals['mod']
174             ip = vals['ip']
175             message = getattr(message, mod)()
176             print 'echo:', message
177             return message
178
179     PyminDaemon(Root()).run()
180