Statistics
| Branch: | Tag: | Revision:

root / daemons / ganeti-confd @ 562bee4d

History | View | Annotate | Download (9 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

    
36
from optparse import OptionParser
37

    
38
from ganeti import constants
39
from ganeti import errors
40
from ganeti import daemon
41
from ganeti import ssconf
42
from ganeti.asyncnotifier import AsyncNotifier
43
from ganeti.confd.server import ConfdProcessor
44

    
45

    
46
class ConfdAsyncUDPServer(asyncore.dispatcher):
47
  """The confd udp server, suitable for use with asyncore.
48

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

    
53
    @type bind_address: string
54
    @param bind_address: socket bind address ('' for all)
55
    @type port: int
56
    @param port: udp port
57
    @type processor: L{confd.server.ConfdProcessor}
58
    @param reader: ConfigReader to use to access the config
59

    
60
    """
61
    asyncore.dispatcher.__init__(self)
62
    self.bind_address = bind_address
63
    self.port = port
64
    self.processor = processor
65
    self.create_socket(socket.AF_INET, socket.SOCK_DGRAM)
66
    self.bind((bind_address, port))
67
    logging.debug("listening on ('%s':%d)" % (bind_address, port))
68

    
69
  # this method is overriding an asyncore.dispatcher method
70
  def handle_connect(self):
71
    # Python thinks that the first udp message from a source qualifies as a
72
    # "connect" and further ones are part of the same connection. We beg to
73
    # differ and treat all messages equally.
74
    pass
75

    
76
  # this method is overriding an asyncore.dispatcher method
77
  def handle_read(self):
78
    try:
79
      payload_in, address = self.recvfrom(4096)
80
      ip, port = address
81
      payload_out =  self.processor.ExecQuery(payload_in, ip, port)
82
      if payload_out is not None:
83
        self.sendto(payload_out, 0, (ip, port))
84
    except:
85
      # we need to catch any exception here, log it, but proceed, because even
86
      # if we failed handling a single request, we still want the confd to
87
      # continue working.
88
      logging.error("Unexpected exception", exc_info=True)
89

    
90
  # this method is overriding an asyncore.dispatcher method
91
  def writable(self):
92
    # No need to check if we can write to the UDP socket
93
    return False
94

    
95

    
96
class ConfdInotifyEventHandler(pyinotify.ProcessEvent):
97

    
98
  def __init__(self, watch_manager, reader,
99
               file=constants.CLUSTER_CONF_FILE):
100
    """Constructor for ConfdInotifyEventHandler
101

    
102
    @type watch_manager: L{pyinotify.WatchManager}
103
    @param watch_manager: ganeti-confd inotify watch manager
104
    @type reader: L{ssconf.SimpleConfigReader}
105
    @param reader: ganeti-confd SimpleConfigReader
106
    @type file: string
107
    @param file: config file to watch
108

    
109
    """
110
    # no need to call the parent's constructor
111
    self.watch_manager = watch_manager
112
    self.reader = reader
113
    self.mask = pyinotify.EventsCodes.IN_IGNORED | \
114
                pyinotify.EventsCodes.IN_MODIFY
115
    self.file = file
116
    self.watch_handle = None
117
    self.enable()
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.ConfdFatalError("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 reload_config(self):
140
    try:
141
      reloaded = self.reader.Reload()
142
      if reloaded:
143
        logging.info("Reloaded ganeti config")
144
      else:
145
        logging.debug("Skipped double config reload")
146
    except errors.ConfigurationError:
147
      # transform a ConfigurationError in a fatal error, that will cause confd
148
      # to quit.
149
      raise errors.ConfdFatalError(err)
150

    
151
  def process_IN_IGNORED(self, event):
152
    # Due to the fact that we monitor just for the cluster config file (rather
153
    # than for the whole data dir) when the file is replaced with another one
154
    # (which is what happens normally in ganeti) we're going to receive an
155
    # IN_IGNORED event from inotify, because of the file removal (which is
156
    # contextual with the replacement). In such a case we need to create
157
    # another watcher for the "new" file.
158
    logging.debug("Received 'ignored' inotify event for %s" % event.path)
159
    self.watch_handle = None
160

    
161
    try:
162
      # Since the kernel believes the file we were interested in is gone, it's
163
      # not going to notify us of any other events, until we set up, here, the
164
      # new watch. This is not a race condition, though, since we're anyway
165
      # going to realod the file after setting up the new watch.
166
      self.enable()
167
      self.reload_config()
168
    except errors.ConfdFatalError, err:
169
      logging.critical("Critical error, shutting down: %s" % err)
170
      sys.exit(constants.EXIT_FAILURE)
171
    except:
172
      # we need to catch any exception here, log it, but proceed, because even
173
      # if we failed handling a single request, we still want the confd to
174
      # continue working.
175
      logging.error("Unexpected exception", exc_info=True)
176

    
177
  def process_IN_MODIFY(self, event):
178
    # This gets called when the config file is modified. Note that this doesn't
179
    # usually happen in Ganeti, as the config file is normally replaced by a
180
    # new one, at filesystem level, rather than actually modified (see
181
    # utils.WriteFile)
182
    logging.debug("Received 'modify' inotify event for %s" % event.path)
183

    
184
    try:
185
      self.reload_config()
186
    except errors.ConfdFatalError, err:
187
      logging.critical("Critical error, shutting down: %s" % err)
188
      sys.exit(constants.EXIT_FAILURE)
189
    except:
190
      # we need to catch any exception here, log it, but proceed, because even
191
      # if we failed handling a single request, we still want the confd to
192
      # continue working.
193
      logging.error("Unexpected exception", exc_info=True)
194

    
195
  def process_default(self, event):
196
    logging.error("Received unhandled inotify event: %s" % event)
197

    
198

    
199
class ConfdConfigurationReloader(object):
200
  """Logic to control when to reload the ganeti configuration
201

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

    
206
  """
207
  def __init__(self, reader):
208
    """Constructor for ConfdConfigurationReloader
209

    
210
    @type reader: L{ssconf.SimpleConfigReader}
211
    @param reader: ganeti-confd SimpleConfigReader
212

    
213
    """
214
    self.reader = reader
215

    
216
    # Asyncronous inotify handler for config changes
217
    self.wm = pyinotify.WatchManager()
218
    self.inotify_handler = ConfdInotifyEventHandler(self.wm, reader)
219
    self.notifier = AsyncNotifier(self.wm, self.confd_event_handler)
220

    
221

    
222
def CheckConfd(options, args):
223
  """Initial checks whether to run exit with a failure.
224

    
225
  """
226
  # TODO: collapse HMAC daemons handling in daemons GenericMain, when we'll
227
  # have more than one.
228
  if not os.path.isfile(constants.HMAC_CLUSTER_KEY):
229
    print >> sys.stderr, "Need HMAC key %s to run" % constants.HMAC_CLUSTER_KEY
230
    sys.exit(constants.EXIT_FAILURE)
231

    
232
  ssconf.CheckMasterCandidate(options.debug)
233

    
234

    
235
def ExecConfd(options, args):
236
  """Main confd function, executed with PID file held
237

    
238
  """
239
  mainloop = daemon.Mainloop()
240

    
241
  # confd-level SimpleConfigReader
242
  reader = ssconf.SimpleConfigReader()
243

    
244
  # Asyncronous confd UDP server
245
  processor = ConfdProcessor(reader)
246
  server = ConfdAsyncUDPServer(options.bind_address, options.port, processor)
247

    
248
  # Configuration reloader
249
  reloader = ConfdConfigurationReloader(reader)
250

    
251
  mainloop.Run()
252

    
253

    
254
def main():
255
  """Main function for the confd daemon.
256

    
257
  """
258
  parser = OptionParser(description="Ganeti configuration daemon",
259
                        usage="%prog [-f] [-d] [-b ADDRESS]",
260
                        version="%%prog (ganeti) %s" %
261
                        constants.RELEASE_VERSION)
262

    
263
  dirs = [(val, constants.RUN_DIRS_MODE) for val in constants.SUB_RUN_DIRS]
264
  dirs.append((constants.LOG_OS_DIR, 0750))
265
  dirs.append((constants.LOCK_DIR, 1777))
266
  daemon.GenericMain(constants.CONFD, parser, dirs, CheckConfd, ExecConfd)
267

    
268

    
269
if __name__ == "__main__":
270
  main()