Statistics
| Branch: | Tag: | Revision:

root / daemons / ganeti-confd @ 6c948699

History | View | Annotate | Download (8 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.add_config_watch()
117

    
118
  def add_config_watch(self):
119
    """Add a watcher for the ganeti config file
120

    
121
    """
122
    result = self.watch_manager.add_watch(self.file, self.mask)
123
    if not result[self.file] > 0:
124
      raise errors.ConfdFatalError("Could not add inotify watcher")
125

    
126
  def reload_config(self):
127
    try:
128
      reloaded = self.reader.Reload()
129
      if reloaded:
130
        logging.info("Reloaded ganeti config")
131
      else:
132
        logging.debug("Skipped double config reload")
133
    except errors.ConfigurationError:
134
      # transform a ConfigurationError in a fatal error, that will cause confd
135
      # to quit.
136
      raise errors.ConfdFatalError(err)
137

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

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

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

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

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

    
184

    
185
def CheckConfd(options, args):
186
  """Initial checks whether to run exit with a failure.
187

    
188
  """
189
  # TODO: collapse HMAC daemons handling in daemons GenericMain, when we'll
190
  # have more than one.
191
  if not os.path.isfile(constants.HMAC_CLUSTER_KEY):
192
    print >> sys.stderr, "Need HMAC key %s to run" % constants.HMAC_CLUSTER_KEY
193
    sys.exit(constants.EXIT_FAILURE)
194

    
195
  ssconf.CheckMasterCandidate(options.debug)
196

    
197

    
198
def ExecConfd(options, args):
199
  """Main confd function, executed with PID file held
200

    
201
  """
202
  mainloop = daemon.Mainloop()
203

    
204
  # confd-level SimpleConfigReader
205
  reader = ssconf.SimpleConfigReader()
206

    
207
  # Asyncronous confd UDP server
208
  processor = ConfdProcessor(reader)
209
  server = ConfdAsyncUDPServer(options.bind_address, options.port, processor)
210

    
211
  # Asyncronous inotify handler for config changes
212
  wm = pyinotify.WatchManager()
213
  confd_event_handler = ConfdInotifyEventHandler(wm, reader)
214
  notifier = AsyncNotifier(wm, confd_event_handler)
215

    
216
  mainloop.Run()
217

    
218

    
219
def main():
220
  """Main function for the confd daemon.
221

    
222
  """
223
  parser = OptionParser(description="Ganeti configuration daemon",
224
                        usage="%prog [-f] [-d] [-b ADDRESS]",
225
                        version="%%prog (ganeti) %s" %
226
                        constants.RELEASE_VERSION)
227

    
228
  dirs = [(val, constants.RUN_DIRS_MODE) for val in constants.SUB_RUN_DIRS]
229
  dirs.append((constants.LOG_OS_DIR, 0750))
230
  dirs.append((constants.LOCK_DIR, 1777))
231
  daemon.GenericMain(constants.CONFD, parser, dirs, CheckConfd, ExecConfd)
232

    
233

    
234
if __name__ == "__main__":
235
  main()