Statistics
| Branch: | Tag: | Revision:

root / daemons / ganeti-confd @ 46c9b31d

History | View | Annotate | Download (8.3 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
def CheckConfd(options, args):
200
  """Initial checks whether to run exit with a failure.
201

    
202
  """
203
  # TODO: collapse HMAC daemons handling in daemons GenericMain, when we'll
204
  # have more than one.
205
  if not os.path.isfile(constants.HMAC_CLUSTER_KEY):
206
    print >> sys.stderr, "Need HMAC key %s to run" % constants.HMAC_CLUSTER_KEY
207
    sys.exit(constants.EXIT_FAILURE)
208

    
209
  ssconf.CheckMasterCandidate(options.debug)
210

    
211

    
212
def ExecConfd(options, args):
213
  """Main confd function, executed with PID file held
214

    
215
  """
216
  mainloop = daemon.Mainloop()
217

    
218
  # confd-level SimpleConfigReader
219
  reader = ssconf.SimpleConfigReader()
220

    
221
  # Asyncronous confd UDP server
222
  processor = ConfdProcessor(reader)
223
  server = ConfdAsyncUDPServer(options.bind_address, options.port, processor)
224

    
225
  # Asyncronous inotify handler for config changes
226
  wm = pyinotify.WatchManager()
227
  confd_event_handler = ConfdInotifyEventHandler(wm, reader)
228
  notifier = AsyncNotifier(wm, confd_event_handler)
229

    
230
  mainloop.Run()
231

    
232

    
233
def main():
234
  """Main function for the confd daemon.
235

    
236
  """
237
  parser = OptionParser(description="Ganeti configuration daemon",
238
                        usage="%prog [-f] [-d] [-b ADDRESS]",
239
                        version="%%prog (ganeti) %s" %
240
                        constants.RELEASE_VERSION)
241

    
242
  dirs = [(val, constants.RUN_DIRS_MODE) for val in constants.SUB_RUN_DIRS]
243
  dirs.append((constants.LOG_OS_DIR, 0750))
244
  dirs.append((constants.LOCK_DIR, 1777))
245
  daemon.GenericMain(constants.CONFD, parser, dirs, CheckConfd, ExecConfd)
246

    
247

    
248
if __name__ == "__main__":
249
  main()