Statistics
| Branch: | Tag: | Revision:

root / daemons / ganeti-confd @ 22d3e184

History | View | Annotate | Download (12.1 kB)

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()
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):
315
    """Puts confd in non-serving mode
316

    
317
    """
318
    logging.warning("Confd is being disabled")
319
    self.processor.Disable()
320
    self.polling = False
321
    self._ResetTimer()
322

    
323
  def EnableConfd(self):
324
    self.processor.Enable()
325
    logging.warning("Confd is being enabled")
326
    self.polling = True
327
    self._ResetTimer()
328

    
329

    
330
def CheckConfd(options, args):
331
  """Initial checks whether to run exit with a failure.
332

    
333
  """
334
  # TODO: collapse HMAC daemons handling in daemons GenericMain, when we'll
335
  # have more than one.
336
  if not os.path.isfile(constants.HMAC_CLUSTER_KEY):
337
    print >> sys.stderr, "Need HMAC key %s to run" % constants.HMAC_CLUSTER_KEY
338
    sys.exit(constants.EXIT_FAILURE)
339

    
340

    
341
def ExecConfd(options, args):
342
  """Main confd function, executed with PID file held
343

    
344
  """
345
  mainloop = daemon.Mainloop()
346

    
347
  # Asyncronous confd UDP server
348
  processor = ConfdProcessor()
349
  try:
350
    processor.Enable()
351
  except errors.ConfigurationError:
352
    # If enabling the processor has failed, we can still go on, but confd will be disabled
353
    pass
354
  server = ConfdAsyncUDPServer(options.bind_address, options.port, processor)
355

    
356
  # Configuration reloader
357
  reloader = ConfdConfigurationReloader(processor, mainloop)
358

    
359
  mainloop.Run()
360

    
361

    
362
def main():
363
  """Main function for the confd daemon.
364

    
365
  """
366
  parser = OptionParser(description="Ganeti configuration daemon",
367
                        usage="%prog [-f] [-d] [-b ADDRESS]",
368
                        version="%%prog (ganeti) %s" %
369
                        constants.RELEASE_VERSION)
370

    
371
  dirs = [(val, constants.RUN_DIRS_MODE) for val in constants.SUB_RUN_DIRS]
372
  dirs.append((constants.LOG_OS_DIR, 0750))
373
  dirs.append((constants.LOCK_DIR, 1777))
374
  daemon.GenericMain(constants.CONFD, parser, dirs, CheckConfd, ExecConfd)
375

    
376

    
377
if __name__ == "__main__":
378
  main()