Statistics
| Branch: | Tag: | Revision:

root / daemons / ganeti-confd @ c9ca81c9

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
# pylint: disable-msg=C0103
30
# C0103: Invalid name ganeti-confd
31

    
32
import os
33
import sys
34
import logging
35
import time
36

    
37
try:
38
  # pylint: disable-msg=E0611
39
  from pyinotify import pyinotify
40
except ImportError:
41
  import pyinotify
42

    
43
from optparse import OptionParser
44

    
45
from ganeti import asyncnotifier
46
from ganeti import confd
47
from ganeti.confd import server as confd_server
48
from ganeti import constants
49
from ganeti import errors
50
from ganeti import daemon
51
from ganeti import ssconf
52

    
53

    
54
class ConfdAsyncUDPServer(daemon.AsyncUDPSocket):
55
  """The confd udp server, suitable for use with asyncore.
56

    
57
  """
58
  def __init__(self, bind_address, port, processor):
59
    """Constructor for ConfdAsyncUDPServer
60

    
61
    @type bind_address: string
62
    @param bind_address: socket bind address ('' for all)
63
    @type port: int
64
    @param port: udp port
65
    @type processor: L{confd.server.ConfdProcessor}
66
    @param processor: ConfdProcessor to use to handle queries
67

    
68
    """
69
    daemon.AsyncUDPSocket.__init__(self)
70
    self.bind_address = bind_address
71
    self.port = port
72
    self.processor = processor
73
    self.bind((bind_address, port))
74
    logging.debug("listening on ('%s':%d)", bind_address, port)
75

    
76
  # this method is overriding a daemon.AsyncUDPSocket method
77
  def handle_datagram(self, payload_in, ip, port):
78
    try:
79
      query = confd.UnpackMagic(payload_in)
80
    except errors.ConfdMagicError, err:
81
      logging.debug(err)
82
      return
83

    
84
    answer =  self.processor.ExecQuery(query, ip, port)
85
    if answer is not None:
86
      try:
87
        self.enqueue_send(ip, port, confd.PackMagic(answer))
88
      except errors.UdpDataSizeError:
89
        logging.error("Reply too big to fit in an udp packet.")
90

    
91

    
92
class ConfdInotifyEventHandler(pyinotify.ProcessEvent):
93

    
94
  def __init__(self, watch_manager, callback,
95
               filename=constants.CLUSTER_CONF_FILE):
96
    """Constructor for ConfdInotifyEventHandler
97

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

    
105
    """
106
    # no need to call the parent's constructor
107
    self.watch_manager = watch_manager
108
    self.callback = callback
109
    # pylint: disable-msg=E1103
110
    # pylint for some reason doesn't see the below constants
111
    self.mask = pyinotify.EventsCodes.IN_IGNORED | \
112
                pyinotify.EventsCodes.IN_MODIFY
113
    self.file = filename
114
    self.watch_handle = None
115

    
116
  def enable(self):
117
    """Watch the given file
118

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

    
127
  def disable(self):
128
    """Stop watching the given file
129

    
130
    """
131
    if self.watch_handle is not None:
132
      result = self.watch_manager.rm_watch(self.watch_handle)
133
      if result[self.watch_handle]:
134
        self.watch_handle = None
135

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

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

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

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

    
179
  def process_default(self, event):
180
    logging.error("Received unhandled inotify event: %s", event)
181

    
182

    
183
class ConfdConfigurationReloader(object):
184
  """Logic to control when to reload the ganeti configuration
185

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

    
190
  """
191
  def __init__(self, processor, mainloop):
192
    """Constructor for ConfdConfigurationReloader
193

    
194
    @type processor: L{confd.server.ConfdProcessor}
195
    @param processor: ganeti-confd ConfdProcessor
196
    @type mainloop: L{daemon.Mainloop}
197
    @param mainloop: ganeti-confd mainloop
198

    
199
    """
200
    self.processor = processor
201
    self.mainloop = mainloop
202

    
203
    self.polling = True
204
    self.last_notification = 0
205

    
206
    # Asyncronous inotify handler for config changes
207
    self.wm = pyinotify.WatchManager()
208
    self.inotify_handler = ConfdInotifyEventHandler(self.wm, self.OnInotify)
209
    self.notifier = asyncnotifier.AsyncNotifier(self.wm, self.inotify_handler)
210

    
211
    self.timer_handle = None
212
    self._EnableTimer()
213

    
214
  def OnInotify(self, notifier_enabled):
215
    """Receive an inotify notification.
216

    
217
    @type notifier_enabled: boolean
218
    @param notifier_enabled: whether the notifier is still enabled
219

    
220
    """
221
    current_time = time.time()
222
    time_delta = current_time - self.last_notification
223
    self.last_notification = current_time
224

    
225
    if time_delta < constants.CONFD_CONFIG_RELOAD_RATELIMIT:
226
      logging.debug("Moving from inotify mode to polling mode")
227
      self.polling = True
228
      if notifier_enabled:
229
        self.inotify_handler.disable()
230

    
231
    if not self.polling and not notifier_enabled:
232
      try:
233
        self.inotify_handler.enable()
234
      except errors.InotifyError:
235
        self.polling = True
236

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

    
248
    # Reset the timer. If we're polling it will go to the polling rate, if
249
    # we're not it will delay it again to its base safe timeout.
250
    self._ResetTimer()
251

    
252
  def _DisableTimer(self):
253
    if self.timer_handle is not None:
254
      self.mainloop.scheduler.cancel(self.timer_handle)
255
      self.timer_handle = None
256

    
257
  def _EnableTimer(self):
258
    if self.polling:
259
      timeout = constants.CONFD_CONFIG_RELOAD_RATELIMIT
260
    else:
261
      timeout = constants.CONFD_CONFIG_RELOAD_TIMEOUT
262

    
263
    if self.timer_handle is None:
264
      self.timer_handle = self.mainloop.scheduler.enter(
265
        timeout, 1, self.OnTimer, [])
266

    
267
  def _ResetTimer(self):
268
    self._DisableTimer()
269
    self._EnableTimer()
270

    
271
  def OnTimer(self):
272
    """Function called when the timer fires
273

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

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

    
309
    self._EnableTimer()
310

    
311
  def DisableConfd(self, silent=False):
312
    """Puts confd in non-serving mode
313

    
314
    """
315
    if not silent:
316
      logging.warning("Confd is being disabled")
317
    self.processor.Disable()
318
    self.polling = False
319
    self._ResetTimer()
320

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

    
327

    
328
def CheckConfd(_, args):
329
  """Initial checks whether to run exit with a failure.
330

    
331
  """
332
  if args: # confd doesn't take any arguments
333
    print >> sys.stderr, ("Usage: %s [-f] [-d] [-b ADDRESS]" % sys.argv[0])
334
    sys.exit(constants.EXIT_FAILURE)
335

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

    
342

    
343
def ExecConfd(options, _):
344
  """Main confd function, executed with PID file held
345

    
346
  """
347
  # TODO: clarify how the server and reloader variables work (they are
348
  # not used)
349
  # pylint: disable-msg=W0612
350
  mainloop = daemon.Mainloop()
351

    
352
  # Asyncronous confd UDP server
353
  processor = confd_server.ConfdProcessor()
354
  try:
355
    processor.Enable()
356
  except errors.ConfigurationError:
357
    # If enabling the processor has failed, we can still go on, but confd will
358
    # be disabled
359
    logging.warning("Confd is starting in disabled mode")
360

    
361
  server = ConfdAsyncUDPServer(options.bind_address, options.port, processor)
362

    
363
  # Configuration reloader
364
  reloader = ConfdConfigurationReloader(processor, mainloop)
365

    
366
  mainloop.Run()
367

    
368

    
369
def main():
370
  """Main function for the confd daemon.
371

    
372
  """
373
  parser = OptionParser(description="Ganeti configuration daemon",
374
                        usage="%prog [-f] [-d] [-b ADDRESS]",
375
                        version="%%prog (ganeti) %s" %
376
                        constants.RELEASE_VERSION)
377

    
378
  dirs = [(val, constants.RUN_DIRS_MODE) for val in constants.SUB_RUN_DIRS]
379
  dirs.append((constants.LOCK_DIR, 1777))
380
  daemon.GenericMain(constants.CONFD, parser, dirs, CheckConfd, ExecConfd)
381

    
382

    
383
if __name__ == "__main__":
384
  main()