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.
36 from optparse import OptionParser
38 from ganeti import constants
39 from ganeti import errors
40 from ganeti import daemon
41 from ganeti import ssconf
42 from ganeti.asyncnotifier import AsyncNotifier
43 from ganeti.confd.server import ConfdProcessor
46 class ConfdAsyncUDPServer(asyncore.dispatcher):
47 """The confd udp server, suitable for use with asyncore.
50 def __init__(self, bind_address, port, processor):
51 """Constructor for ConfdAsyncUDPServer
53 @type bind_address: string
54 @param bind_address: socket bind address ('' for all)
57 @type processor: L{confd.server.ConfdProcessor}
58 @param reader: ConfigReader to use to access the config
61 asyncore.dispatcher.__init__(self)
62 self.bind_address = bind_address
64 self.processor = processor
65 self.create_socket(socket.AF_INET, socket.SOCK_DGRAM)
66 self.bind((bind_address, port))
67 logging.debug("listening on ('%s':%d)" % (bind_address, port))
69 # this method is overriding an asyncore.dispatcher method
70 def handle_connect(self):
71 # Python thinks that the first udp message from a source qualifies as a
72 # "connect" and further ones are part of the same connection. We beg to
73 # differ and treat all messages equally.
76 # this method is overriding an asyncore.dispatcher method
77 def handle_read(self):
79 payload_in, address = self.recvfrom(4096)
81 payload_out = self.processor.ExecQuery(payload_in, ip, port)
82 if payload_out is not None:
83 self.sendto(payload_out, 0, (ip, port))
85 # we need to catch any exception here, log it, but proceed, because even
86 # if we failed handling a single request, we still want the confd to
88 logging.error("Unexpected exception", exc_info=True)
90 # this method is overriding an asyncore.dispatcher method
92 # No need to check if we can write to the UDP socket
96 class ConfdInotifyEventHandler(pyinotify.ProcessEvent):
98 def __init__(self, watch_manager, reader,
99 file=constants.CLUSTER_CONF_FILE):
100 """Constructor for ConfdInotifyEventHandler
102 @type watch_manager: L{pyinotify.WatchManager}
103 @param watch_manager: ganeti-confd inotify watch manager
104 @type reader: L{ssconf.SimpleConfigReader}
105 @param reader: ganeti-confd SimpleConfigReader
107 @param file: config file to watch
110 # no need to call the parent's constructor
111 self.watch_manager = watch_manager
113 self.mask = pyinotify.EventsCodes.IN_IGNORED | \
114 pyinotify.EventsCodes.IN_MODIFY
116 self.add_config_watch()
118 def add_config_watch(self):
119 """Add a watcher for the ganeti config file
122 result = self.watch_manager.add_watch(self.file, self.mask)
123 if not result[self.file] > 0:
124 raise errors.ConfdFatalError("Could not add inotify watcher")
126 def reload_config(self):
128 reloaded = self.reader.Reload()
130 logging.info("Reloaded ganeti config")
132 logging.debug("Skipped double config reload")
133 except errors.ConfigurationError:
134 # transform a ConfigurationError in a fatal error, that will cause confd
136 raise errors.ConfdFatalError(err)
138 def process_IN_IGNORED(self, event):
139 # Due to the fact that we monitor just for the cluster config file (rather
140 # than for the whole data dir) when the file is replaced with another one
141 # (which is what happens normally in ganeti) we're going to receive an
142 # IN_IGNORED event from inotify, because of the file removal (which is
143 # contextual with the replacement). In such a case we need to create
144 # another watcher for the "new" file.
145 logging.debug("Received 'ignored' inotify event for %s" % event.path)
148 # Since the kernel believes the file we were interested in is gone, it's
149 # not going to notify us of any other events, until we set up, here, the
150 # new watch. This is not a race condition, though, since we're anyway
151 # going to realod the file after setting up the new watch.
152 self.add_config_watch()
154 except errors.ConfdFatalError, err:
155 logging.critical("Critical error, shutting down: %s" % err)
156 sys.exit(constants.EXIT_FAILURE)
158 # we need to catch any exception here, log it, but proceed, because even
159 # if we failed handling a single request, we still want the confd to
161 logging.error("Unexpected exception", exc_info=True)
163 def process_IN_MODIFY(self, event):
164 # This gets called when the config file is modified. Note that this doesn't
165 # usually happen in Ganeti, as the config file is normally replaced by a
166 # new one, at filesystem level, rather than actually modified (see
168 logging.debug("Received 'modify' inotify event for %s" % event.path)
172 except errors.ConfdFatalError, err:
173 logging.critical("Critical error, shutting down: %s" % err)
174 sys.exit(constants.EXIT_FAILURE)
176 # we need to catch any exception here, log it, but proceed, because even
177 # if we failed handling a single request, we still want the confd to
179 logging.error("Unexpected exception", exc_info=True)
181 def process_default(self, event):
182 logging.error("Received unhandled inotify event: %s" % event)
185 def CheckCONFD(options, args):
186 """Initial checks whether to run exit with a failure
189 # TODO: collapse HMAC daemons handling in daemons GenericMain, when we'll
190 # have more than one.
191 if not os.path.isfile(constants.HMAC_CLUSTER_KEY):
192 print >> sys.stderr, "Need HMAC key %s to run" % constants.HMAC_CLUSTER_KEY
193 sys.exit(constants.EXIT_FAILURE)
195 ssconf.CheckMasterCandidate(options.debug)
198 def ExecCONFD(options, args):
199 """Main CONFD function, executed with pidfile held
202 mainloop = daemon.Mainloop()
204 # confd-level SimpleConfigReader
205 reader = ssconf.SimpleConfigReader()
207 # Asyncronous confd UDP server
208 processor = ConfdProcessor(reader)
209 server = ConfdAsyncUDPServer(options.bind_address, options.port, processor)
211 # Asyncronous inotify handler for config changes
212 wm = pyinotify.WatchManager()
213 confd_event_handler = ConfdInotifyEventHandler(wm, reader)
214 notifier = AsyncNotifier(wm, confd_event_handler)
220 """Main function for the confd daemon.
223 parser = OptionParser(description="Ganeti configuration daemon",
224 usage="%prog [-f] [-d] [-b ADDRESS]",
225 version="%%prog (ganeti) %s" %
226 constants.RELEASE_VERSION)
228 dirs = [(val, constants.RUN_DIRS_MODE) for val in constants.SUB_RUN_DIRS]
229 dirs.append((constants.LOG_OS_DIR, 0750))
230 dirs.append((constants.LOCK_DIR, 1777))
231 daemon.GenericMain(constants.CONFD, parser, dirs, CheckCONFD, ExecCONFD)
234 if __name__ == '__main__':