Merge branch 'next' into branch-2.1
[ganeti-local] / lib / daemon.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007, 2008 Google Inc.
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 # 02110-1301, USA.
20
21
22 """Module with helper classes and functions for daemons"""
23
24
25 import asyncore
26 import os
27 import select
28 import signal
29 import errno
30 import logging
31 import sched
32 import time
33
34 from ganeti import utils
35 from ganeti import constants
36 from ganeti import errors
37
38
39 class SchedulerBreakout(Exception):
40   """Exception used to get out of the scheduler loop
41
42   """
43
44
45 def AsyncoreDelayFunction(timeout):
46   """Asyncore-compatible scheduler delay function.
47
48   This is a delay function for sched that, rather than actually sleeping,
49   executes asyncore events happening in the meantime.
50
51   After an event has occurred, rather than returning, it raises a
52   SchedulerBreakout exception, which will force the current scheduler.run()
53   invocation to terminate, so that we can also check for signals. The main loop
54   will then call the scheduler run again, which will allow it to actually
55   process any due events.
56
57   This is needed because scheduler.run() doesn't support a count=..., as
58   asyncore loop, and the scheduler module documents throwing exceptions from
59   inside the delay function as an allowed usage model.
60
61   """
62   asyncore.loop(timeout=timeout, count=1, use_poll=True)
63   raise SchedulerBreakout()
64
65
66 class AsyncoreScheduler(sched.scheduler):
67   """Event scheduler integrated with asyncore
68
69   """
70   def __init__(self, timefunc):
71     sched.scheduler.__init__(self, timefunc, AsyncoreDelayFunction)
72
73
74 class Mainloop(object):
75   """Generic mainloop for daemons
76
77   """
78   def __init__(self):
79     """Constructs a new Mainloop instance.
80
81     @ivar scheduler: A L{sched.scheduler} object, which can be used to register
82     timed events
83
84     """
85     self._signal_wait = []
86     self.scheduler = AsyncoreScheduler(time.time)
87
88   @utils.SignalHandled([signal.SIGCHLD])
89   @utils.SignalHandled([signal.SIGTERM])
90   def Run(self, stop_on_empty=False, signal_handlers=None):
91     """Runs the mainloop.
92
93     @type stop_on_empty: bool
94     @param stop_on_empty: Whether to stop mainloop once all I/O waiters
95                           unregistered
96     @type signal_handlers: dict
97     @param signal_handlers: signal->L{utils.SignalHandler} passed by decorator
98
99     """
100     assert isinstance(signal_handlers, dict) and \
101            len(signal_handlers) > 0, \
102            "Broken SignalHandled decorator"
103     running = True
104     # Start actual main loop
105     while running:
106       # Stop if nothing is listening anymore
107       if stop_on_empty and not (self._io_wait):
108         break
109
110       if not self.scheduler.empty():
111         try:
112           self.scheduler.run()
113         except SchedulerBreakout:
114           pass
115       else:
116         asyncore.loop(count=1, use_poll=True)
117
118       # Check whether a signal was raised
119       for sig in signal_handlers:
120         handler = signal_handlers[sig]
121         if handler.called:
122           self._CallSignalWaiters(sig)
123           running = (sig != signal.SIGTERM)
124           handler.Clear()
125
126   def _CallSignalWaiters(self, signum):
127     """Calls all signal waiters for a certain signal.
128
129     @type signum: int
130     @param signum: Signal number
131
132     """
133     for owner in self._signal_wait:
134       owner.OnSignal(signum)
135
136   def RegisterSignal(self, owner):
137     """Registers a receiver for signal notifications
138
139     The receiver must support a "OnSignal(self, signum)" function.
140
141     @type owner: instance
142     @param owner: Receiver
143
144     """
145     self._signal_wait.append(owner)
146
147
148 def GenericMain(daemon_name, optionparser, dirs, check_fn, exec_fn):
149   """Shared main function for daemons.
150
151   @type daemon_name: string
152   @param daemon_name: daemon name
153   @type optionparser: L{optparse.OptionParser}
154   @param optionparser: initialized optionparser with daemon-specific options
155                        (common -f -d options will be handled by this module)
156   @type options: object @param options: OptionParser result, should contain at
157                  least the fork and the debug options
158   @type dirs: list of strings
159   @param dirs: list of directories that must exist for this daemon to work
160   @type check_fn: function which accepts (options, args)
161   @param check_fn: function that checks start conditions and exits if they're
162                    not met
163   @type exec_fn: function which accepts (options, args)
164   @param exec_fn: function that's executed with the daemon's pid file held, and
165                   runs the daemon itself.
166
167   """
168   optionparser.add_option("-f", "--foreground", dest="fork",
169                           help="Don't detach from the current terminal",
170                           default=True, action="store_false")
171   optionparser.add_option("-d", "--debug", dest="debug",
172                           help="Enable some debug messages",
173                           default=False, action="store_true")
174   if daemon_name in constants.DAEMONS_PORTS:
175     # for networked daemons we also allow choosing the bind port and address.
176     # by default we use the port provided by utils.GetDaemonPort, and bind to
177     # 0.0.0.0 (which is represented by and empty bind address.
178     port = utils.GetDaemonPort(daemon_name)
179     optionparser.add_option("-p", "--port", dest="port",
180                             help="Network port (%s default)." % port,
181                             default=port, type="int")
182     optionparser.add_option("-b", "--bind", dest="bind_address",
183                             help="Bind address",
184                             default="", metavar="ADDRESS")
185
186   if daemon_name in constants.DAEMONS_SSL:
187     default_cert, default_key = constants.DAEMONS_SSL[daemon_name]
188     optionparser.add_option("--no-ssl", dest="ssl",
189                             help="Do not secure HTTP protocol with SSL",
190                             default=True, action="store_false")
191     optionparser.add_option("-K", "--ssl-key", dest="ssl_key",
192                             help="SSL key",
193                             default=default_key, type="string")
194     optionparser.add_option("-C", "--ssl-cert", dest="ssl_cert",
195                             help="SSL certificate",
196                             default=default_cert, type="string")
197
198   multithread = utils.no_fork = daemon_name in constants.MULTITHREADED_DAEMONS
199
200   options, args = optionparser.parse_args()
201
202   if hasattr(options, 'ssl') and options.ssl:
203     if not (options.ssl_cert and options.ssl_key):
204       print >> sys.stderr, "Need key and certificate to use ssl"
205       sys.exit(constants.EXIT_FAILURE)
206     for fname in (options.ssl_cert, options.ssl_key):
207       if not os.path.isfile(fname):
208         print >> sys.stderr, "Need ssl file %s to run" % fname
209         sys.exit(constants.EXIT_FAILURE)
210
211   if check_fn is not None:
212     check_fn(options, args)
213
214   utils.EnsureDirs(dirs)
215
216   if options.fork:
217     utils.CloseFDs()
218     utils.Daemonize(logfile=constants.DAEMONS_LOGFILES[daemon_name])
219
220   utils.WritePidFile(daemon_name)
221   try:
222     utils.SetupLogging(logfile=constants.DAEMONS_LOGFILES[daemon_name],
223                        debug=options.debug,
224                        stderr_logging=not options.fork,
225                        multithreaded=multithread)
226     logging.info("%s daemon startup" % daemon_name)
227     exec_fn(options, args)
228   finally:
229     utils.RemovePidFile(daemon_name)
230