Style fixes for ganeti-*
[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
36 from optparse import OptionParser
37
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
44
45
46 class ConfdAsyncUDPServer(asyncore.dispatcher):
47   """The confd udp server, suitable for use with asyncore.
48
49   """
50   def __init__(self, bind_address, port, processor):
51     """Constructor for ConfdAsyncUDPServer
52
53     @type bind_address: string
54     @param bind_address: socket bind address ('' for all)
55     @type port: int
56     @param port: udp port
57     @type processor: L{confd.server.ConfdProcessor}
58     @param reader: ConfigReader to use to access the config
59
60     """
61     asyncore.dispatcher.__init__(self)
62     self.bind_address = bind_address
63     self.port = port
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))
68
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.
74     pass
75
76   # this method is overriding an asyncore.dispatcher method
77   def handle_read(self):
78     try:
79       payload_in, address = self.recvfrom(4096)
80       ip, port = address
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))
84     except:
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
87       # continue working.
88       logging.error("Unexpected exception", exc_info=True)
89
90   # this method is overriding an asyncore.dispatcher method
91   def writable(self):
92     # No need to check if we can write to the UDP socket
93     return False
94
95
96 class ConfdInotifyEventHandler(pyinotify.ProcessEvent):
97
98   def __init__(self, watch_manager, reader,
99                file=constants.CLUSTER_CONF_FILE):
100     """Constructor for ConfdInotifyEventHandler
101
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
106     @type file: string
107     @param file: config file to watch
108
109     """
110     # no need to call the parent's constructor
111     self.watch_manager = watch_manager
112     self.reader = reader
113     self.mask = pyinotify.EventsCodes.IN_IGNORED | \
114                 pyinotify.EventsCodes.IN_MODIFY
115     self.file = file
116     self.add_config_watch()
117
118   def add_config_watch(self):
119     """Add a watcher for the ganeti config file
120
121     """
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")
125
126   def reload_config(self):
127     try:
128       reloaded = self.reader.Reload()
129       if reloaded:
130         logging.info("Reloaded ganeti config")
131       else:
132         logging.debug("Skipped double config reload")
133     except errors.ConfigurationError:
134       # transform a ConfigurationError in a fatal error, that will cause confd
135       # to quit.
136       raise errors.ConfdFatalError(err)
137
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)
146
147     try:
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()
153       self.reload_config()
154     except errors.ConfdFatalError, err:
155       logging.critical("Critical error, shutting down: %s" % err)
156       sys.exit(constants.EXIT_FAILURE)
157     except:
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
160       # continue working.
161       logging.error("Unexpected exception", exc_info=True)
162
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
167     # utils.WriteFile)
168     logging.debug("Received 'modify' inotify event for %s" % event.path)
169
170     try:
171       self.reload_config()
172     except errors.ConfdFatalError, err:
173       logging.critical("Critical error, shutting down: %s" % err)
174       sys.exit(constants.EXIT_FAILURE)
175     except:
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
178       # continue working.
179       logging.error("Unexpected exception", exc_info=True)
180
181   def process_default(self, event):
182     logging.error("Received unhandled inotify event: %s" % event)
183
184
185 def CheckConfd(options, args):
186   """Initial checks whether to run exit with a failure.
187
188   """
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)
194
195   ssconf.CheckMasterCandidate(options.debug)
196
197
198 def ExecConfd(options, args):
199   """Main confd function, executed with PID file held
200
201   """
202   mainloop = daemon.Mainloop()
203
204   # confd-level SimpleConfigReader
205   reader = ssconf.SimpleConfigReader()
206
207   # Asyncronous confd UDP server
208   processor = ConfdProcessor(reader)
209   server = ConfdAsyncUDPServer(options.bind_address, options.port, processor)
210
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)
215
216   mainloop.Run()
217
218
219 def main():
220   """Main function for the confd daemon.
221
222   """
223   parser = OptionParser(description="Ganeti configuration daemon",
224                         usage="%prog [-f] [-d] [-b ADDRESS]",
225                         version="%%prog (ganeti) %s" %
226                         constants.RELEASE_VERSION)
227
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)
232
233
234 if __name__ == "__main__":
235   main()