4 # Copyright (C) 2009, Google Inc.
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.
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.
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
22 """Ganeti configuration daemon
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.
38 from optparse import OptionParser
40 from ganeti import constants
41 from ganeti import errors
42 from ganeti import daemon
43 from ganeti import ssconf
44 from ganeti.asyncnotifier import AsyncNotifier
45 from ganeti.confd.server import ConfdProcessor
46 from ganeti.confd import PackMagic, UnpackMagic
49 class ConfdAsyncUDPServer(daemon.AsyncUDPSocket):
50 """The confd udp server, suitable for use with asyncore.
53 def __init__(self, bind_address, port, processor):
54 """Constructor for ConfdAsyncUDPServer
56 @type bind_address: string
57 @param bind_address: socket bind address ('' for all)
60 @type processor: L{confd.server.ConfdProcessor}
61 @param processor: ConfdProcessor to use to handle queries
64 daemon.AsyncUDPSocket.__init__(self)
65 self.bind_address = bind_address
67 self.processor = processor
68 self.bind((bind_address, port))
69 logging.debug("listening on ('%s':%d)" % (bind_address, port))
71 # this method is overriding a daemon.AsyncUDPSocket method
72 def handle_datagram(self, payload_in, ip, port):
74 query = UnpackMagic(payload_in)
75 except errors.ConfdMagicError, err:
79 answer = self.processor.ExecQuery(query, ip, port)
80 if answer is not None:
82 self.enqueue_send(ip, port, PackMagic(answer))
83 except errors.UdpDataSizeError:
84 logging.error("Reply too big to fit in an udp packet.")
87 class ConfdInotifyEventHandler(pyinotify.ProcessEvent):
89 def __init__(self, watch_manager, callback,
90 file=constants.CLUSTER_CONF_FILE):
91 """Constructor for ConfdInotifyEventHandler
93 @type watch_manager: L{pyinotify.WatchManager}
94 @param watch_manager: ganeti-confd inotify watch manager
95 @type callback: function accepting a boolean
96 @param callback: function to call when an inotify event happens
98 @param file: config file to watch
101 # no need to call the parent's constructor
102 self.watch_manager = watch_manager
103 self.callback = callback
104 self.mask = pyinotify.EventsCodes.IN_IGNORED | \
105 pyinotify.EventsCodes.IN_MODIFY
107 self.watch_handle = None
110 """Watch the given file
113 if self.watch_handle is None:
114 result = self.watch_manager.add_watch(self.file, self.mask)
115 if not self.file in result or result[self.file] <= 0:
116 raise errors.InotifyError("Could not add inotify watcher")
118 self.watch_handle = result[self.file]
121 """Stop watching the given file
124 if self.watch_handle is not None:
125 result = self.watch_manager.rm_watch(self.watch_handle)
126 if result[self.watch_handle]:
127 self.watch_handle = None
129 def process_IN_IGNORED(self, event):
130 # Due to the fact that we monitor just for the cluster config file (rather
131 # than for the whole data dir) when the file is replaced with another one
132 # (which is what happens normally in ganeti) we're going to receive an
133 # IN_IGNORED event from inotify, because of the file removal (which is
134 # contextual with the replacement). In such a case we need to create
135 # another watcher for the "new" file.
136 logging.debug("Received 'ignored' inotify event for %s" % event.path)
137 self.watch_handle = None
140 # Since the kernel believes the file we were interested in is gone, it's
141 # not going to notify us of any other events, until we set up, here, the
142 # new watch. This is not a race condition, though, since we're anyway
143 # going to realod the file after setting up the new watch.
145 except errors.ConfdFatalError, err:
146 logging.critical("Critical error, shutting down: %s" % err)
147 sys.exit(constants.EXIT_FAILURE)
149 # we need to catch any exception here, log it, but proceed, because even
150 # if we failed handling a single request, we still want the confd to
152 logging.error("Unexpected exception", exc_info=True)
154 def process_IN_MODIFY(self, event):
155 # This gets called when the config file is modified. Note that this doesn't
156 # usually happen in Ganeti, as the config file is normally replaced by a
157 # new one, at filesystem level, rather than actually modified (see
159 logging.debug("Received 'modify' inotify event for %s" % event.path)
163 except errors.ConfdFatalError, err:
164 logging.critical("Critical error, shutting down: %s" % err)
165 sys.exit(constants.EXIT_FAILURE)
167 # we need to catch any exception here, log it, but proceed, because even
168 # if we failed handling a single request, we still want the confd to
170 logging.error("Unexpected exception", exc_info=True)
172 def process_default(self, event):
173 logging.error("Received unhandled inotify event: %s" % event)
176 class ConfdConfigurationReloader(object):
177 """Logic to control when to reload the ganeti configuration
179 This class is able to alter between inotify and polling, to rate-limit the
180 number of reloads. When using inotify it also supports a fallback timed
181 check, to verify that the reload hasn't failed.
184 def __init__(self, processor, mainloop):
185 """Constructor for ConfdConfigurationReloader
187 @type processor: L{confd.server.ConfdProcessor}
188 @param processor: ganeti-confd ConfdProcessor
189 @type mainloop: L{daemon.Mainloop}
190 @param mainloop: ganeti-confd mainloop
193 self.processor = processor
194 self.mainloop = mainloop
197 self.last_notification = 0
199 # Asyncronous inotify handler for config changes
200 self.wm = pyinotify.WatchManager()
201 self.inotify_handler = ConfdInotifyEventHandler(self.wm, self.OnInotify)
202 self.notifier = AsyncNotifier(self.wm, self.inotify_handler)
204 self.timer_handle = None
207 def OnInotify(self, notifier_enabled):
208 """Receive an inotify notification.
210 @type notifier_enabled: boolean
211 @param notifier_enabled: whether the notifier is still enabled
214 current_time = time.time()
215 time_delta = current_time - self.last_notification
216 self.last_notification = current_time
218 if time_delta < constants.CONFD_CONFIG_RELOAD_RATELIMIT:
219 logging.debug("Moving from inotify mode to polling mode")
222 self.inotify_handler.disable()
224 if not self.polling and not notifier_enabled:
226 self.inotify_handler.enable()
227 except errors.InotifyError:
231 reloaded = self.processor.reader.Reload()
233 logging.info("Reloaded ganeti config")
235 logging.debug("Skipped double config reload")
236 except errors.ConfigurationError:
238 self.inotify_handler.disable()
241 # Reset the timer. If we're polling it will go to the polling rate, if
242 # we're not it will delay it again to its base safe timeout.
245 def _DisableTimer(self):
246 if self.timer_handle is not None:
247 self.mainloop.scheduler.cancel(self.timer_handle)
248 self.timer_handle = None
250 def _EnableTimer(self):
252 timeout = constants.CONFD_CONFIG_RELOAD_RATELIMIT
254 timeout = constants.CONFD_CONFIG_RELOAD_TIMEOUT
256 if self.timer_handle is None:
257 self.timer_handle = self.mainloop.scheduler.enter(
258 timeout, 1, self.OnTimer, [])
260 def _ResetTimer(self):
265 """Function called when the timer fires
268 self.timer_handle = None
272 if self.processor.reader is None:
277 reloaded = self.processor.reader.Reload()
278 except errors.ConfigurationError:
279 self.DisableConfd(silent=was_disabled)
282 if self.polling and reloaded:
283 logging.info("Reloaded ganeti config")
285 # We have reloaded the config files, but received no inotify event. If
286 # an event is pending though, we just happen to have timed out before
287 # receiving it, so this is not a problem, and we shouldn't alert
288 if not self.notifier.check_events() and not was_disabled:
289 logging.warning("Config file reload at timeout (inotify failure)")
291 # We're polling, but we haven't reloaded the config:
292 # Going back to inotify mode
293 logging.debug("Moving from polling mode to inotify mode")
296 self.inotify_handler.enable()
297 except errors.InotifyError:
300 logging.debug("Performed configuration check")
304 def DisableConfd(self, silent=False):
305 """Puts confd in non-serving mode
309 logging.warning("Confd is being disabled")
310 self.processor.Disable()
314 def EnableConfd(self):
315 self.processor.Enable()
316 logging.warning("Confd is being enabled")
321 def CheckConfd(options, args):
322 """Initial checks whether to run exit with a failure.
325 # TODO: collapse HMAC daemons handling in daemons GenericMain, when we'll
326 # have more than one.
327 if not os.path.isfile(constants.HMAC_CLUSTER_KEY):
328 print >> sys.stderr, "Need HMAC key %s to run" % constants.HMAC_CLUSTER_KEY
329 sys.exit(constants.EXIT_FAILURE)
332 def ExecConfd(options, args):
333 """Main confd function, executed with PID file held
336 mainloop = daemon.Mainloop()
338 # Asyncronous confd UDP server
339 processor = ConfdProcessor()
342 except errors.ConfigurationError:
343 # If enabling the processor has failed, we can still go on, but confd will
345 logging.warning("Confd is starting in disabled mode")
347 server = ConfdAsyncUDPServer(options.bind_address, options.port, processor)
349 # Configuration reloader
350 reloader = ConfdConfigurationReloader(processor, mainloop)
356 """Main function for the confd daemon.
359 parser = OptionParser(description="Ganeti configuration daemon",
360 usage="%prog [-f] [-d] [-b ADDRESS]",
361 version="%%prog (ganeti) %s" %
362 constants.RELEASE_VERSION)
364 dirs = [(val, constants.RUN_DIRS_MODE) for val in constants.SUB_RUN_DIRS]
365 dirs.append((constants.LOG_OS_DIR, 0750))
366 dirs.append((constants.LOCK_DIR, 1777))
367 daemon.GenericMain(constants.CONFD, parser, dirs, CheckConfd, ExecConfd)
370 if __name__ == "__main__":