Wrap lines over 80 characters
[ganeti-local] / daemons / ganeti-confd
1 #!/usr/bin/python
2 #
3
4 # Copyright (C) 2009, 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 """Ganeti configuration daemon
23
24 Ganeti-confd is a daemon to query master candidates for configuration values.
25 It uses UDP+HMAC for authentication with a global cluster key.
26
27 """
28
29 import os
30 import sys
31 import logging
32 import asyncore
33 import socket
34 import pyinotify
35 import time
36
37 from optparse import OptionParser
38
39 from ganeti import constants
40 from ganeti import errors
41 from ganeti import daemon
42 from ganeti import ssconf
43 from ganeti.asyncnotifier import AsyncNotifier
44 from ganeti.confd.server import ConfdProcessor
45
46
47 class ConfdAsyncUDPServer(asyncore.dispatcher):
48   """The confd udp server, suitable for use with asyncore.
49
50   """
51   def __init__(self, bind_address, port, processor):
52     """Constructor for ConfdAsyncUDPServer
53
54     @type bind_address: string
55     @param bind_address: socket bind address ('' for all)
56     @type port: int
57     @param port: udp port
58     @type processor: L{confd.server.ConfdProcessor}
59     @param reader: ConfigReader to use to access the config
60
61     """
62     asyncore.dispatcher.__init__(self)
63     self.bind_address = bind_address
64     self.port = port
65     self.processor = processor
66     self.create_socket(socket.AF_INET, socket.SOCK_DGRAM)
67     self.bind((bind_address, port))
68     logging.debug("listening on ('%s':%d)" % (bind_address, port))
69
70   # this method is overriding an asyncore.dispatcher method
71   def handle_connect(self):
72     # Python thinks that the first udp message from a source qualifies as a
73     # "connect" and further ones are part of the same connection. We beg to
74     # differ and treat all messages equally.
75     pass
76
77   # this method is overriding an asyncore.dispatcher method
78   def handle_read(self):
79     try:
80       payload_in, address = self.recvfrom(4096)
81       ip, port = address
82       payload_out =  self.processor.ExecQuery(payload_in, ip, port)
83       if payload_out is not None:
84         self.sendto(payload_out, 0, (ip, port))
85     except:
86       # we need to catch any exception here, log it, but proceed, because even
87       # if we failed handling a single request, we still want the confd to
88       # continue working.
89       logging.error("Unexpected exception", exc_info=True)
90
91   # this method is overriding an asyncore.dispatcher method
92   def writable(self):
93     # No need to check if we can write to the UDP socket
94     return False
95
96
97 class ConfdInotifyEventHandler(pyinotify.ProcessEvent):
98
99   def __init__(self, watch_manager, callback,
100                file=constants.CLUSTER_CONF_FILE):
101     """Constructor for ConfdInotifyEventHandler
102
103     @type watch_manager: L{pyinotify.WatchManager}
104     @param watch_manager: ganeti-confd inotify watch manager
105     @type callback: function accepting a boolean
106     @param callback: function to call when an inotify event happens
107     @type file: string
108     @param file: config file to watch
109
110     """
111     # no need to call the parent's constructor
112     self.watch_manager = watch_manager
113     self.callback = callback
114     self.mask = pyinotify.EventsCodes.IN_IGNORED | \
115                 pyinotify.EventsCodes.IN_MODIFY
116     self.file = file
117     self.watch_handle = None
118
119   def enable(self):
120     """Watch the given file
121
122     """
123     if self.watch_handle is None:
124       result = self.watch_manager.add_watch(self.file, self.mask)
125       if not self.file in result or result[self.file] <= 0:
126         raise errors.InotifyError("Could not add inotify watcher")
127       else:
128         self.watch_handle = result[self.file]
129
130   def disable(self):
131     """Stop watching the given file
132
133     """
134     if self.watch_handle is not None:
135       result = self.watch_manager.rm_watch(self.watch_handle)
136       if result[self.watch_handle]:
137         self.watch_handle = None
138
139   def process_IN_IGNORED(self, event):
140     # Due to the fact that we monitor just for the cluster config file (rather
141     # than for the whole data dir) when the file is replaced with another one
142     # (which is what happens normally in ganeti) we're going to receive an
143     # IN_IGNORED event from inotify, because of the file removal (which is
144     # contextual with the replacement). In such a case we need to create
145     # another watcher for the "new" file.
146     logging.debug("Received 'ignored' inotify event for %s" % event.path)
147     self.watch_handle = None
148
149     try:
150       # Since the kernel believes the file we were interested in is gone, it's
151       # not going to notify us of any other events, until we set up, here, the
152       # new watch. This is not a race condition, though, since we're anyway
153       # going to realod the file after setting up the new watch.
154       self.callback(False)
155     except errors.ConfdFatalError, err:
156       logging.critical("Critical error, shutting down: %s" % err)
157       sys.exit(constants.EXIT_FAILURE)
158     except:
159       # we need to catch any exception here, log it, but proceed, because even
160       # if we failed handling a single request, we still want the confd to
161       # continue working.
162       logging.error("Unexpected exception", exc_info=True)
163
164   def process_IN_MODIFY(self, event):
165     # This gets called when the config file is modified. Note that this doesn't
166     # usually happen in Ganeti, as the config file is normally replaced by a
167     # new one, at filesystem level, rather than actually modified (see
168     # utils.WriteFile)
169     logging.debug("Received 'modify' inotify event for %s" % event.path)
170
171     try:
172       self.callback(True)
173     except errors.ConfdFatalError, err:
174       logging.critical("Critical error, shutting down: %s" % err)
175       sys.exit(constants.EXIT_FAILURE)
176     except:
177       # we need to catch any exception here, log it, but proceed, because even
178       # if we failed handling a single request, we still want the confd to
179       # continue working.
180       logging.error("Unexpected exception", exc_info=True)
181
182   def process_default(self, event):
183     logging.error("Received unhandled inotify event: %s" % event)
184
185
186 class ConfdConfigurationReloader(object):
187   """Logic to control when to reload the ganeti configuration
188
189   This class is able to alter between inotify and polling, to rate-limit the
190   number of reloads. When using inotify it also supports a fallback timed
191   check, to verify that the reload hasn't failed.
192
193   """
194   def __init__(self, processor, mainloop):
195     """Constructor for ConfdConfigurationReloader
196
197     @type processor: L{confd.server.ConfdProcessor}
198     @param processor: ganeti-confd ConfdProcessor
199     @type mainloop: L{daemon.Mainloop}
200     @param mainloop: ganeti-confd mainloop
201
202     """
203     self.processor = processor
204     self.mainloop = mainloop
205
206     self.polling = True
207     self.last_notification = 0
208
209     # Asyncronous inotify handler for config changes
210     self.wm = pyinotify.WatchManager()
211     self.inotify_handler = ConfdInotifyEventHandler(self.wm, self.OnInotify)
212     self.notifier = AsyncNotifier(self.wm, self.inotify_handler)
213
214     self.timer_handle = None
215     self._EnableTimer()
216
217   def OnInotify(self, notifier_enabled):
218     """Receive an inotify notification.
219
220     @type notifier_enabled: boolean
221     @param notifier_enabled: whether the notifier is still enabled
222
223     """
224     current_time = time.time()
225     time_delta = current_time - self.last_notification
226     self.last_notification = current_time
227
228     if time_delta < constants.CONFD_CONFIG_RELOAD_RATELIMIT:
229       logging.debug("Moving from inotify mode to polling mode")
230       self.polling = True
231       if notifier_enabled:
232         self.inotify_handler.disable()
233
234     if not self.polling and not notifier_enabled:
235       try:
236         self.inotify_handler.enable()
237       except errors.InotifyError:
238         self.polling = True
239
240     try:
241       reloaded = self.processor.reader.Reload()
242       if reloaded:
243         logging.info("Reloaded ganeti config")
244       else:
245         logging.debug("Skipped double config reload")
246     except errors.ConfigurationError:
247       self.DisableConfd()
248       self.inotify_handler.disable()
249       return
250
251     # Reset the timer. If we're polling it will go to the polling rate, if
252     # we're not it will delay it again to its base safe timeout.
253     self._ResetTimer()
254
255   def _DisableTimer(self):
256     if self.timer_handle is not None:
257       self.mainloop.scheduler.cancel(self.timer_handle)
258       self.timer_handle = None
259
260   def _EnableTimer(self):
261     if self.polling:
262       timeout = constants.CONFD_CONFIG_RELOAD_RATELIMIT
263     else:
264       timeout = constants.CONFD_CONFIG_RELOAD_TIMEOUT
265
266     if self.timer_handle is None:
267       self.timer_handle = self.mainloop.scheduler.enter(
268         timeout, 1, self.OnTimer, [])
269
270   def _ResetTimer(self):
271     self._DisableTimer()
272     self._EnableTimer()
273
274   def OnTimer(self):
275     """Function called when the timer fires
276
277     """
278     self.timer_handle = None
279     reloaded = False
280     was_disabled = False
281     try:
282       if self.processor.reader is None:
283         was_disabled = True
284         self.EnableConfd()
285         reloaded = True
286       else:
287         reloaded = self.processor.reader.Reload()
288     except errors.ConfigurationError:
289       self.DisableConfd(silent=was_disabled)
290       return
291
292     if self.polling and reloaded:
293       logging.info("Reloaded ganeti config")
294     elif reloaded:
295       # We have reloaded the config files, but received no inotify event.  If
296       # an event is pending though, we just happen to have timed out before
297       # receiving it, so this is not a problem, and we shouldn't alert
298       if not self.notifier.check_events() and not was_disabled:
299         logging.warning("Config file reload at timeout (inotify failure)")
300     elif self.polling:
301       # We're polling, but we haven't reloaded the config:
302       # Going back to inotify mode
303       logging.debug("Moving from polling mode to inotify mode")
304       self.polling = False
305       try:
306         self.inotify_handler.enable()
307       except errors.InotifyError:
308         self.polling = True
309     else:
310       logging.debug("Performed configuration check")
311
312     self._EnableTimer()
313
314   def DisableConfd(self, silent=False):
315     """Puts confd in non-serving mode
316
317     """
318     if not silent:
319       logging.warning("Confd is being disabled")
320     self.processor.Disable()
321     self.polling = False
322     self._ResetTimer()
323
324   def EnableConfd(self):
325     self.processor.Enable()
326     logging.warning("Confd is being enabled")
327     self.polling = True
328     self._ResetTimer()
329
330
331 def CheckConfd(options, args):
332   """Initial checks whether to run exit with a failure.
333
334   """
335   # TODO: collapse HMAC daemons handling in daemons GenericMain, when we'll
336   # have more than one.
337   if not os.path.isfile(constants.HMAC_CLUSTER_KEY):
338     print >> sys.stderr, "Need HMAC key %s to run" % constants.HMAC_CLUSTER_KEY
339     sys.exit(constants.EXIT_FAILURE)
340
341
342 def ExecConfd(options, args):
343   """Main confd function, executed with PID file held
344
345   """
346   mainloop = daemon.Mainloop()
347
348   # Asyncronous confd UDP server
349   processor = ConfdProcessor()
350   try:
351     processor.Enable()
352   except errors.ConfigurationError:
353     # If enabling the processor has failed, we can still go on, but confd will
354     # be disabled
355     logging.warning("Confd is starting in disabled mode")
356     pass
357   server = ConfdAsyncUDPServer(options.bind_address, options.port, processor)
358
359   # Configuration reloader
360   reloader = ConfdConfigurationReloader(processor, mainloop)
361
362   mainloop.Run()
363
364
365 def main():
366   """Main function for the confd daemon.
367
368   """
369   parser = OptionParser(description="Ganeti configuration daemon",
370                         usage="%prog [-f] [-d] [-b ADDRESS]",
371                         version="%%prog (ganeti) %s" %
372                         constants.RELEASE_VERSION)
373
374   dirs = [(val, constants.RUN_DIRS_MODE) for val in constants.SUB_RUN_DIRS]
375   dirs.append((constants.LOG_OS_DIR, 0750))
376   dirs.append((constants.LOCK_DIR, 1777))
377   daemon.GenericMain(constants.CONFD, parser, dirs, CheckConfd, ExecConfd)
378
379
380 if __name__ == "__main__":
381   main()