Statistics
| Branch: | Tag: | Revision:

root / lib / server / confd.py @ cca5b3fc

History | View | Annotate | Download (9.1 kB)

1
#
2
#
3

    
4
# Copyright (C) 2009, 2010 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=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=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 netutils
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
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
    family = netutils.IPAddress.GetAddressFamily(bind_address)
70
    daemon.AsyncUDPSocket.__init__(self, family)
71
    self.bind_address = bind_address
72
    self.port = port
73
    self.processor = processor
74
    self.bind((bind_address, port))
75
    logging.debug("listening on ('%s':%d)", bind_address, port)
76

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

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

    
92

    
93
class ConfdConfigurationReloader(object):
94
  """Logic to control when to reload the ganeti configuration
95

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

100
  """
101
  def __init__(self, processor, mainloop):
102
    """Constructor for ConfdConfigurationReloader
103

104
    @type processor: L{confd.server.ConfdProcessor}
105
    @param processor: ganeti-confd ConfdProcessor
106
    @type mainloop: L{daemon.Mainloop}
107
    @param mainloop: ganeti-confd mainloop
108

109
    """
110
    self.processor = processor
111
    self.mainloop = mainloop
112

    
113
    self.polling = True
114
    self.last_notification = 0
115

    
116
    # Asyncronous inotify handler for config changes
117
    cfg_file = constants.CLUSTER_CONF_FILE
118
    self.wm = pyinotify.WatchManager()
119
    self.inotify_handler = asyncnotifier.SingleFileEventHandler(self.wm,
120
                                                                self.OnInotify,
121
                                                                cfg_file)
122
    notifier_class = asyncnotifier.ErrorLoggingAsyncNotifier
123
    self.notifier = notifier_class(self.wm, self.inotify_handler)
124

    
125
    self.timer_handle = None
126
    self._EnableTimer()
127

    
128
  def OnInotify(self, notifier_enabled):
129
    """Receive an inotify notification.
130

131
    @type notifier_enabled: boolean
132
    @param notifier_enabled: whether the notifier is still enabled
133

134
    """
135
    current_time = time.time()
136
    time_delta = current_time - self.last_notification
137
    self.last_notification = current_time
138

    
139
    if time_delta < constants.CONFD_CONFIG_RELOAD_RATELIMIT:
140
      logging.debug("Moving from inotify mode to polling mode")
141
      self.polling = True
142
      if notifier_enabled:
143
        self.inotify_handler.disable()
144

    
145
    if not self.polling and not notifier_enabled:
146
      try:
147
        self.inotify_handler.enable()
148
      except errors.InotifyError:
149
        self.polling = True
150

    
151
    try:
152
      reloaded = self.processor.reader.Reload()
153
      if reloaded:
154
        logging.info("Reloaded ganeti config")
155
      else:
156
        logging.debug("Skipped double config reload")
157
    except errors.ConfigurationError:
158
      self.DisableConfd()
159
      self.inotify_handler.disable()
160
      return
161

    
162
    # Reset the timer. If we're polling it will go to the polling rate, if
163
    # we're not it will delay it again to its base safe timeout.
164
    self._ResetTimer()
165

    
166
  def _DisableTimer(self):
167
    if self.timer_handle is not None:
168
      self.mainloop.scheduler.cancel(self.timer_handle)
169
      self.timer_handle = None
170

    
171
  def _EnableTimer(self):
172
    if self.polling:
173
      timeout = constants.CONFD_CONFIG_RELOAD_RATELIMIT
174
    else:
175
      timeout = constants.CONFD_CONFIG_RELOAD_TIMEOUT
176

    
177
    if self.timer_handle is None:
178
      self.timer_handle = self.mainloop.scheduler.enter(
179
        timeout, 1, self.OnTimer, [])
180

    
181
  def _ResetTimer(self):
182
    self._DisableTimer()
183
    self._EnableTimer()
184

    
185
  def OnTimer(self):
186
    """Function called when the timer fires
187

188
    """
189
    self.timer_handle = None
190
    reloaded = False
191
    was_disabled = False
192
    try:
193
      if self.processor.reader is None:
194
        was_disabled = True
195
        self.EnableConfd()
196
        reloaded = True
197
      else:
198
        reloaded = self.processor.reader.Reload()
199
    except errors.ConfigurationError:
200
      self.DisableConfd(silent=was_disabled)
201
      return
202

    
203
    if self.polling and reloaded:
204
      logging.info("Reloaded ganeti config")
205
    elif reloaded:
206
      # We have reloaded the config files, but received no inotify event.  If
207
      # an event is pending though, we just happen to have timed out before
208
      # receiving it, so this is not a problem, and we shouldn't alert
209
      if not self.notifier.check_events() and not was_disabled:
210
        logging.warning("Config file reload at timeout (inotify failure)")
211
    elif self.polling:
212
      # We're polling, but we haven't reloaded the config:
213
      # Going back to inotify mode
214
      logging.debug("Moving from polling mode to inotify mode")
215
      self.polling = False
216
      try:
217
        self.inotify_handler.enable()
218
      except errors.InotifyError:
219
        self.polling = True
220
    else:
221
      logging.debug("Performed configuration check")
222

    
223
    self._EnableTimer()
224

    
225
  def DisableConfd(self, silent=False):
226
    """Puts confd in non-serving mode
227

228
    """
229
    if not silent:
230
      logging.warning("Confd is being disabled")
231
    self.processor.Disable()
232
    self.polling = False
233
    self._ResetTimer()
234

    
235
  def EnableConfd(self):
236
    self.processor.Enable()
237
    logging.warning("Confd is being enabled")
238
    self.polling = True
239
    self._ResetTimer()
240

    
241

    
242
def CheckConfd(_, args):
243
  """Initial checks whether to run exit with a failure.
244

245
  """
246
  if args: # confd doesn't take any arguments
247
    print >> sys.stderr, ("Usage: %s [-f] [-d] [-b ADDRESS]" % sys.argv[0])
248
    sys.exit(constants.EXIT_FAILURE)
249

    
250
  # TODO: collapse HMAC daemons handling in daemons GenericMain, when we'll
251
  # have more than one.
252
  if not os.path.isfile(constants.CONFD_HMAC_KEY):
253
    print >> sys.stderr, "Need HMAC key %s to run" % constants.CONFD_HMAC_KEY
254
    sys.exit(constants.EXIT_FAILURE)
255

    
256
  # TODO: once we have a cluster param specifying the address family
257
  # preference, we need to check if the requested options.bind_address does not
258
  # conflict with that. If so, we might warn or EXIT_FAILURE.
259

    
260

    
261
def PrepConfd(options, _):
262
  """Prep confd function, executed with PID file held
263

264
  """
265
  # TODO: clarify how the server and reloader variables work (they are
266
  # not used)
267

    
268
  # pylint: disable=W0612
269
  mainloop = daemon.Mainloop()
270

    
271
  # Asyncronous confd UDP server
272
  processor = confd_server.ConfdProcessor()
273
  try:
274
    processor.Enable()
275
  except errors.ConfigurationError:
276
    # If enabling the processor has failed, we can still go on, but confd will
277
    # be disabled
278
    logging.warning("Confd is starting in disabled mode")
279

    
280
  server = ConfdAsyncUDPServer(options.bind_address, options.port, processor)
281

    
282
  # Configuration reloader
283
  reloader = ConfdConfigurationReloader(processor, mainloop)
284

    
285
  return mainloop
286

    
287

    
288
def ExecConfd(options, args, prep_data): # pylint: disable=W0613
289
  """Main confd function, executed with PID file held
290

291
  """
292
  mainloop = prep_data
293
  mainloop.Run()
294

    
295

    
296
def Main():
297
  """Main function for the confd daemon.
298

299
  """
300
  parser = OptionParser(description="Ganeti configuration daemon",
301
                        usage="%prog [-f] [-d] [-b ADDRESS]",
302
                        version="%%prog (ganeti) %s" %
303
                        constants.RELEASE_VERSION)
304

    
305
  daemon.GenericMain(constants.CONFD, parser, CheckConfd, PrepConfd, ExecConfd)