Statistics
| Branch: | Tag: | Revision:

root / daemons / ganeti-confd @ a3758ab2

History | View | Annotate | Download (11.8 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
import errno
37

    
38
from optparse import OptionParser
39

    
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

    
47

    
48
class ConfdAsyncUDPServer(daemon.AsyncUDPSocket):
49
  """The confd udp server, suitable for use with asyncore.
50

    
51
  """
52
  def __init__(self, bind_address, port, processor):
53
    """Constructor for ConfdAsyncUDPServer
54

    
55
    @type bind_address: string
56
    @param bind_address: socket bind address ('' for all)
57
    @type port: int
58
    @param port: udp port
59
    @type processor: L{confd.server.ConfdProcessor}
60
    @param processor: ConfdProcessor to use to handle queries
61

    
62
    """
63
    daemon.AsyncUDPSocket.__init__(self)
64
    self.bind_address = bind_address
65
    self.port = port
66
    self.processor = processor
67
    self.bind((bind_address, port))
68
    logging.debug("listening on ('%s':%d)" % (bind_address, port))
69

    
70
  # this method is overriding a daemon.AsyncUDPSocket method
71
  def handle_datagram(self, payload_in, ip, port):
72

    
73
    if len(payload_in) < len(constants.CONFD_MAGIC_FOURCC):
74
      logging.debug("Received a query which is too short to be true")
75
      return
76

    
77
    magic_number = payload_in[:4]
78
    query = payload_in[4:]
79

    
80
    if magic_number != constants.CONFD_MAGIC_FOURCC:
81
      logging.debug("Received a query with an unknown magic number")
82
      return
83

    
84
    answer =  self.processor.ExecQuery(query, ip, port)
85
    if answer is not None:
86
      payload_out = ''.join([constants.CONFD_MAGIC_FOURCC, answer])
87
      self.enqueue_send(ip, port, payload_out)
88

    
89

    
90
class ConfdInotifyEventHandler(pyinotify.ProcessEvent):
91

    
92
  def __init__(self, watch_manager, callback,
93
               file=constants.CLUSTER_CONF_FILE):
94
    """Constructor for ConfdInotifyEventHandler
95

    
96
    @type watch_manager: L{pyinotify.WatchManager}
97
    @param watch_manager: ganeti-confd inotify watch manager
98
    @type callback: function accepting a boolean
99
    @param callback: function to call when an inotify event happens
100
    @type file: string
101
    @param file: config file to watch
102

    
103
    """
104
    # no need to call the parent's constructor
105
    self.watch_manager = watch_manager
106
    self.callback = callback
107
    self.mask = pyinotify.EventsCodes.IN_IGNORED | \
108
                pyinotify.EventsCodes.IN_MODIFY
109
    self.file = file
110
    self.watch_handle = None
111

    
112
  def enable(self):
113
    """Watch the given file
114

    
115
    """
116
    if self.watch_handle is None:
117
      result = self.watch_manager.add_watch(self.file, self.mask)
118
      if not self.file in result or result[self.file] <= 0:
119
        raise errors.InotifyError("Could not add inotify watcher")
120
      else:
121
        self.watch_handle = result[self.file]
122

    
123
  def disable(self):
124
    """Stop watching the given file
125

    
126
    """
127
    if self.watch_handle is not None:
128
      result = self.watch_manager.rm_watch(self.watch_handle)
129
      if result[self.watch_handle]:
130
        self.watch_handle = None
131

    
132
  def process_IN_IGNORED(self, event):
133
    # Due to the fact that we monitor just for the cluster config file (rather
134
    # than for the whole data dir) when the file is replaced with another one
135
    # (which is what happens normally in ganeti) we're going to receive an
136
    # IN_IGNORED event from inotify, because of the file removal (which is
137
    # contextual with the replacement). In such a case we need to create
138
    # another watcher for the "new" file.
139
    logging.debug("Received 'ignored' inotify event for %s" % event.path)
140
    self.watch_handle = None
141

    
142
    try:
143
      # Since the kernel believes the file we were interested in is gone, it's
144
      # not going to notify us of any other events, until we set up, here, the
145
      # new watch. This is not a race condition, though, since we're anyway
146
      # going to realod the file after setting up the new watch.
147
      self.callback(False)
148
    except errors.ConfdFatalError, err:
149
      logging.critical("Critical error, shutting down: %s" % err)
150
      sys.exit(constants.EXIT_FAILURE)
151
    except:
152
      # we need to catch any exception here, log it, but proceed, because even
153
      # if we failed handling a single request, we still want the confd to
154
      # continue working.
155
      logging.error("Unexpected exception", exc_info=True)
156

    
157
  def process_IN_MODIFY(self, event):
158
    # This gets called when the config file is modified. Note that this doesn't
159
    # usually happen in Ganeti, as the config file is normally replaced by a
160
    # new one, at filesystem level, rather than actually modified (see
161
    # utils.WriteFile)
162
    logging.debug("Received 'modify' inotify event for %s" % event.path)
163

    
164
    try:
165
      self.callback(True)
166
    except errors.ConfdFatalError, err:
167
      logging.critical("Critical error, shutting down: %s" % err)
168
      sys.exit(constants.EXIT_FAILURE)
169
    except:
170
      # we need to catch any exception here, log it, but proceed, because even
171
      # if we failed handling a single request, we still want the confd to
172
      # continue working.
173
      logging.error("Unexpected exception", exc_info=True)
174

    
175
  def process_default(self, event):
176
    logging.error("Received unhandled inotify event: %s" % event)
177

    
178

    
179
class ConfdConfigurationReloader(object):
180
  """Logic to control when to reload the ganeti configuration
181

    
182
  This class is able to alter between inotify and polling, to rate-limit the
183
  number of reloads. When using inotify it also supports a fallback timed
184
  check, to verify that the reload hasn't failed.
185

    
186
  """
187
  def __init__(self, processor, mainloop):
188
    """Constructor for ConfdConfigurationReloader
189

    
190
    @type processor: L{confd.server.ConfdProcessor}
191
    @param processor: ganeti-confd ConfdProcessor
192
    @type mainloop: L{daemon.Mainloop}
193
    @param mainloop: ganeti-confd mainloop
194

    
195
    """
196
    self.processor = processor
197
    self.mainloop = mainloop
198

    
199
    self.polling = True
200
    self.last_notification = 0
201

    
202
    # Asyncronous inotify handler for config changes
203
    self.wm = pyinotify.WatchManager()
204
    self.inotify_handler = ConfdInotifyEventHandler(self.wm, self.OnInotify)
205
    self.notifier = AsyncNotifier(self.wm, self.inotify_handler)
206

    
207
    self.timer_handle = None
208
    self._EnableTimer()
209

    
210
  def OnInotify(self, notifier_enabled):
211
    """Receive an inotify notification.
212

    
213
    @type notifier_enabled: boolean
214
    @param notifier_enabled: whether the notifier is still enabled
215

    
216
    """
217
    current_time = time.time()
218
    time_delta = current_time - self.last_notification
219
    self.last_notification = current_time
220

    
221
    if time_delta < constants.CONFD_CONFIG_RELOAD_RATELIMIT:
222
      logging.debug("Moving from inotify mode to polling mode")
223
      self.polling = True
224
      if notifier_enabled:
225
        self.inotify_handler.disable()
226

    
227
    if not self.polling and not notifier_enabled:
228
      try:
229
        self.inotify_handler.enable()
230
      except errors.InotifyError:
231
        self.polling = True
232

    
233
    try:
234
      reloaded = self.processor.reader.Reload()
235
      if reloaded:
236
        logging.info("Reloaded ganeti config")
237
      else:
238
        logging.debug("Skipped double config reload")
239
    except errors.ConfigurationError:
240
      self.DisableConfd()
241
      self.inotify_handler.disable()
242
      return
243

    
244
    # Reset the timer. If we're polling it will go to the polling rate, if
245
    # we're not it will delay it again to its base safe timeout.
246
    self._ResetTimer()
247

    
248
  def _DisableTimer(self):
249
    if self.timer_handle is not None:
250
      self.mainloop.scheduler.cancel(self.timer_handle)
251
      self.timer_handle = None
252

    
253
  def _EnableTimer(self):
254
    if self.polling:
255
      timeout = constants.CONFD_CONFIG_RELOAD_RATELIMIT
256
    else:
257
      timeout = constants.CONFD_CONFIG_RELOAD_TIMEOUT
258

    
259
    if self.timer_handle is None:
260
      self.timer_handle = self.mainloop.scheduler.enter(
261
        timeout, 1, self.OnTimer, [])
262

    
263
  def _ResetTimer(self):
264
    self._DisableTimer()
265
    self._EnableTimer()
266

    
267
  def OnTimer(self):
268
    """Function called when the timer fires
269

    
270
    """
271
    self.timer_handle = None
272
    reloaded = False
273
    was_disabled = False
274
    try:
275
      if self.processor.reader is None:
276
        was_disabled = True
277
        self.EnableConfd()
278
        reloaded = True
279
      else:
280
        reloaded = self.processor.reader.Reload()
281
    except errors.ConfigurationError:
282
      self.DisableConfd(silent=was_disabled)
283
      return
284

    
285
    if self.polling and reloaded:
286
      logging.info("Reloaded ganeti config")
287
    elif reloaded:
288
      # We have reloaded the config files, but received no inotify event.  If
289
      # an event is pending though, we just happen to have timed out before
290
      # receiving it, so this is not a problem, and we shouldn't alert
291
      if not self.notifier.check_events() and not was_disabled:
292
        logging.warning("Config file reload at timeout (inotify failure)")
293
    elif self.polling:
294
      # We're polling, but we haven't reloaded the config:
295
      # Going back to inotify mode
296
      logging.debug("Moving from polling mode to inotify mode")
297
      self.polling = False
298
      try:
299
        self.inotify_handler.enable()
300
      except errors.InotifyError:
301
        self.polling = True
302
    else:
303
      logging.debug("Performed configuration check")
304

    
305
    self._EnableTimer()
306

    
307
  def DisableConfd(self, silent=False):
308
    """Puts confd in non-serving mode
309

    
310
    """
311
    if not silent:
312
      logging.warning("Confd is being disabled")
313
    self.processor.Disable()
314
    self.polling = False
315
    self._ResetTimer()
316

    
317
  def EnableConfd(self):
318
    self.processor.Enable()
319
    logging.warning("Confd is being enabled")
320
    self.polling = True
321
    self._ResetTimer()
322

    
323

    
324
def CheckConfd(options, args):
325
  """Initial checks whether to run exit with a failure.
326

    
327
  """
328
  # TODO: collapse HMAC daemons handling in daemons GenericMain, when we'll
329
  # have more than one.
330
  if not os.path.isfile(constants.HMAC_CLUSTER_KEY):
331
    print >> sys.stderr, "Need HMAC key %s to run" % constants.HMAC_CLUSTER_KEY
332
    sys.exit(constants.EXIT_FAILURE)
333

    
334

    
335
def ExecConfd(options, args):
336
  """Main confd function, executed with PID file held
337

    
338
  """
339
  mainloop = daemon.Mainloop()
340

    
341
  # Asyncronous confd UDP server
342
  processor = ConfdProcessor()
343
  try:
344
    processor.Enable()
345
  except errors.ConfigurationError:
346
    # If enabling the processor has failed, we can still go on, but confd will
347
    # be disabled
348
    logging.warning("Confd is starting in disabled mode")
349
    pass
350
  server = ConfdAsyncUDPServer(options.bind_address, options.port, processor)
351

    
352
  # Configuration reloader
353
  reloader = ConfdConfigurationReloader(processor, mainloop)
354

    
355
  mainloop.Run()
356

    
357

    
358
def main():
359
  """Main function for the confd daemon.
360

    
361
  """
362
  parser = OptionParser(description="Ganeti configuration daemon",
363
                        usage="%prog [-f] [-d] [-b ADDRESS]",
364
                        version="%%prog (ganeti) %s" %
365
                        constants.RELEASE_VERSION)
366

    
367
  dirs = [(val, constants.RUN_DIRS_MODE) for val in constants.SUB_RUN_DIRS]
368
  dirs.append((constants.LOG_OS_DIR, 0750))
369
  dirs.append((constants.LOCK_DIR, 1777))
370
  daemon.GenericMain(constants.CONFD, parser, dirs, CheckConfd, ExecConfd)
371

    
372

    
373
if __name__ == "__main__":
374
  main()