ganeti-confd 7.97 KB
Newer Older
Guido Trotter's avatar
Guido Trotter committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
#!/usr/bin/python
#

# Copyright (C) 2009, Google Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.


"""Ganeti configuration daemon

Ganeti-confd is a daemon to query master candidates for configuration values.
It uses UDP+HMAC for authentication with a global cluster key.

"""

import os
import sys
import logging
import asyncore
import socket
import pyinotify

from optparse import OptionParser

from ganeti import constants
from ganeti import errors
from ganeti import daemon
from ganeti import ssconf
from ganeti.asyncnotifier import AsyncNotifier
from ganeti.confd.server import ConfdProcessor


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

  """
  def __init__(self, bind_address, port, processor):
    """Constructor for ConfdAsyncUDPServer

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

    """
    asyncore.dispatcher.__init__(self)
    self.bind_address = bind_address
    self.port = port
    self.processor = processor
    self.create_socket(socket.AF_INET, socket.SOCK_DGRAM)
    self.bind((bind_address, port))
    logging.debug("listening on ('%s':%d)" % (bind_address, port))

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

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

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


class ConfdInotifyEventHandler(pyinotify.ProcessEvent):

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

    @type watch_manager: L{pyinotify.WatchManager}
    @param watch_manager: ganeti-confd inotify watch manager
    @type reader: L{ssconf.SimpleConfigReader}
    @param reader: ganeti-confd SimpleConfigReader
    @type file: string
    @param file: config file to watch

    """
    # no need to call the parent's constructor
    self.watch_manager = watch_manager
    self.reader = reader
    self.mask = pyinotify.EventsCodes.IN_IGNORED | \
                pyinotify.EventsCodes.IN_MODIFY
    self.file = file
    self.add_config_watch()

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

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

  def reload_config(self):
    try:
      reloaded = self.reader.Reload()
      if reloaded:
        logging.info("Reloaded ganeti config")
      else:
        logging.debug("Skipped double config reload")
    except errors.ConfigurationError:
      # transform a ConfigurationError in a fatal error, that will cause confd
      # to quit.
      raise errors.ConfdFatalError(err)

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

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

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

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

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


def CheckCONFD(options, args):
  """Initial checks whether to run exit with a failure

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

  ssconf.CheckMasterCandidate(options.debug)


def ExecCONFD(options, args):
  """Main CONFD function, executed with pidfile held

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

Guido Trotter's avatar
Guido Trotter committed
204
205
206
207
208
209
210
211
212
213
214
  # confd-level SimpleConfigReader
  reader = ssconf.SimpleConfigReader()

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

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

  mainloop.Run()
Guido Trotter's avatar
Guido Trotter committed
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235


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

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

  dirs = [(val, constants.RUN_DIRS_MODE) for val in constants.SUB_RUN_DIRS]
  dirs.append((constants.LOG_OS_DIR, 0750))
  dirs.append((constants.LOCK_DIR, 1777))
  daemon.GenericMain(constants.CONFD, parser, dirs, CheckCONFD, ExecCONFD)


if __name__ == '__main__':
  main()