Revision 5c9c0e0e

b/.gitignore
36 36
/daemons/daemon-util
37 37
/daemons/ensure-dirs
38 38
/daemons/ganeti-cleaner
39
/daemons/ganeti-confd
39 40
/daemons/ganeti-masterd
40 41
/daemons/ganeti-watcher
41 42

  
b/Makefile.am
197 197

  
198 198
server_PYTHON = \
199 199
	lib/server/__init__.py \
200
	lib/server/confd.py \
200 201
	lib/server/masterd.py
201 202

  
202 203
docrst = \
......
281 282
	scripts/gnt-os
282 283

  
283 284
PYTHON_BOOTSTRAP = \
285
	daemons/ganeti-confd \
284 286
	daemons/ganeti-masterd \
285 287
	daemons/ganeti-watcher \
286 288
	scripts/gnt-backup \
......
293 295

  
294 296
dist_sbin_SCRIPTS = \
295 297
	daemons/ganeti-noded \
296
	daemons/ganeti-confd \
297 298
	daemons/ganeti-rapi
298 299

  
299 300
nodist_sbin_SCRIPTS = \
/dev/null
1
#!/usr/bin/python
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-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 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-msg=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-msg=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)
306

  
307

  
308
if __name__ == "__main__":
309
  main()
b/lib/server/confd.py
1
#!/usr/bin/python
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-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 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-msg=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-msg=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)

Also available in: Unified diff