ganeti-confd 7.94 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 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232
#!/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

  """
  # 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)
  asyncore.loop()


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()