Revision b0dcdc10

b/.gitignore
41 41
/daemons/daemon-util
42 42
/daemons/ganeti-cleaner
43 43
/daemons/ganeti-master-cleaner
44
/daemons/ganeti-confd
45 44
/daemons/ganeti-masterd
46 45
/daemons/ganeti-noded
47 46
/daemons/ganeti-rapi
b/Makefile.am
169 169
	$(nodist_pkgpython_PYTHON) \
170 170
	$(HS_ALL_PROGS) $(HS_BUILT_SRCS) \
171 171
	$(HS_BUILT_TEST_HELPERS) \
172
	htools/ganeti-confd \
172 173
	.hpc/*.mix htools/*.tix htest/*.tix \
173 174
	doc/hs-lint.html
174 175

  
......
292 293

  
293 294
confd_PYTHON = \
294 295
	lib/confd/__init__.py \
295
	lib/confd/client.py \
296
	lib/confd/querylib.py \
297
	lib/confd/server.py
296
	lib/confd/client.py
298 297

  
299 298
masterd_PYTHON = \
300 299
	lib/masterd/__init__.py \
......
311 310

  
312 311
server_PYTHON = \
313 312
	lib/server/__init__.py \
314
	lib/server/confd.py \
315 313
	lib/server/masterd.py \
316 314
	lib/server/noded.py \
317 315
	lib/server/rapi.py
......
600 598
		$(LN_S) -f htools \
601 599
			   $(DESTDIR)$(bindir)/$$role ; \
602 600
	done
603
if ENABLE_CONFD
604
	mv $(DESTDIR)$(sbindir)/hconfd $(DESTDIR)$(sbindir)/ganeti-confd
605
endif
606 601
endif
607 602

  
608 603
$(HS_ALL_PROGS): %: %.hs $(HS_LIBTEST_SRCS) $(HS_BUILT_SRCS) Makefile
......
654 649
	daemons/ganeti-master-cleaner
655 650

  
656 651
if ENABLE_CONFD
657
nodist_sbin_SCRIPTS += htools/hconfd
652
htools/ganeti-confd: htools/hconfd
653
	cp -f $< $@
654

  
655
nodist_sbin_SCRIPTS += htools/ganeti-confd
658 656
endif
659 657

  
660 658
python_scripts = \
/dev/null
1
#
2
#
3

  
4
# Copyright (C) 2009, 2012 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 queries library.
23

  
24
"""
25

  
26
import logging
27

  
28
from ganeti import constants
29

  
30

  
31
# constants for some common errors to return from a query
32
QUERY_UNKNOWN_ENTRY_ERROR = (constants.CONFD_REPL_STATUS_ERROR,
33
                             constants.CONFD_ERROR_UNKNOWN_ENTRY)
34
QUERY_INTERNAL_ERROR = (constants.CONFD_REPL_STATUS_ERROR,
35
                        constants.CONFD_ERROR_INTERNAL)
36
QUERY_ARGUMENT_ERROR = (constants.CONFD_REPL_STATUS_ERROR,
37
                        constants.CONFD_ERROR_ARGUMENT)
38

  
39

  
40
class ConfdQuery(object):
41
  """Confd Query base class.
42

  
43
  """
44
  def __init__(self, reader):
45
    """Constructor for Confd Query
46

  
47
    @type reader: L{ssconf.SimpleConfigReader}
48
    @param reader: ConfigReader to use to access the config
49

  
50
    """
51
    self.reader = reader
52

  
53
  def Exec(self, query): # pylint: disable=R0201,W0613
54
    """Process a single UDP request from a client.
55

  
56
    Different queries should override this function, which by defaults returns
57
    a "non-implemented" answer.
58

  
59
    @type query: (undefined)
60
    @param query: ConfdRequest 'query' field
61
    @rtype: (integer, undefined)
62
    @return: status and answer to give to the client
63

  
64
    """
65
    status = constants.CONFD_REPL_STATUS_NOTIMPLEMENTED
66
    answer = "not implemented"
67
    return status, answer
68

  
69

  
70
class PingQuery(ConfdQuery):
71
  """An empty confd query.
72

  
73
  It will return success on an empty argument, and an error on any other
74
  argument.
75

  
76
  """
77
  def Exec(self, query):
78
    """PingQuery main execution.
79

  
80
    """
81
    if query is None:
82
      status = constants.CONFD_REPL_STATUS_OK
83
      answer = "ok"
84
    else:
85
      status = constants.CONFD_REPL_STATUS_ERROR
86
      answer = "non-empty ping query"
87

  
88
    return status, answer
89

  
90

  
91
class ClusterMasterQuery(ConfdQuery):
92
  """Cluster master query.
93

  
94
  It accepts no arguments, and returns the current cluster master.
95

  
96
  """
97
  def _GetMasterNode(self):
98
    return self.reader.GetMasterNode()
99

  
100
  def Exec(self, query):
101
    """ClusterMasterQuery main execution
102

  
103
    """
104
    if isinstance(query, dict):
105
      if constants.CONFD_REQQ_FIELDS in query:
106
        status = constants.CONFD_REPL_STATUS_OK
107
        req_fields = query[constants.CONFD_REQQ_FIELDS]
108
        if not isinstance(req_fields, (list, tuple)):
109
          logging.debug("FIELDS request should be a list")
110
          return QUERY_ARGUMENT_ERROR
111

  
112
        answer = []
113
        for field in req_fields:
114
          if field == constants.CONFD_REQFIELD_NAME:
115
            answer.append(self._GetMasterNode())
116
          elif field == constants.CONFD_REQFIELD_IP:
117
            answer.append(self.reader.GetMasterIP())
118
          elif field == constants.CONFD_REQFIELD_MNODE_PIP:
119
            answer.append(self.reader.GetNodePrimaryIp(self._GetMasterNode()))
120
      else:
121
        logging.debug("missing FIELDS in query dict")
122
        return QUERY_ARGUMENT_ERROR
123
    elif not query:
124
      status = constants.CONFD_REPL_STATUS_OK
125
      answer = self.reader.GetMasterNode()
126
    else:
127
      logging.debug("Invalid master query argument: not dict or empty")
128
      return QUERY_ARGUMENT_ERROR
129

  
130
    return status, answer
131

  
132

  
133
class NodeRoleQuery(ConfdQuery):
134
  """A query for the role of a node.
135

  
136
  It will return one of CONFD_NODE_ROLE_*, or an error for non-existing nodes.
137

  
138
  """
139
  def Exec(self, query):
140
    """EmptyQuery main execution
141

  
142
    """
143
    node = query
144
    if self.reader.GetMasterNode() == node:
145
      status = constants.CONFD_REPL_STATUS_OK
146
      answer = constants.CONFD_NODE_ROLE_MASTER
147
      return status, answer
148
    flags = self.reader.GetNodeStatusFlags(node)
149
    if flags is None:
150
      return QUERY_UNKNOWN_ENTRY_ERROR
151

  
152
    master_candidate, drained, offline = flags
153
    if master_candidate:
154
      answer = constants.CONFD_NODE_ROLE_CANDIDATE
155
    elif drained:
156
      answer = constants.CONFD_NODE_ROLE_DRAINED
157
    elif offline:
158
      answer = constants.CONFD_NODE_ROLE_OFFLINE
159
    else:
160
      answer = constants.CONFD_NODE_ROLE_REGULAR
161

  
162
    return constants.CONFD_REPL_STATUS_OK, answer
163

  
164

  
165
class InstanceIpToNodePrimaryIpQuery(ConfdQuery):
166
  """A query for the location of one or more instance's ips.
167

  
168
  """
169
  def Exec(self, query):
170
    """InstanceIpToNodePrimaryIpQuery main execution.
171

  
172
    @type query: string or dict
173
    @param query: instance ip or dict containing:
174
                  constants.CONFD_REQQ_LINK: nic link (optional)
175
                  constants.CONFD_REQQ_IPLIST: list of ips
176
                  constants.CONFD_REQQ_IP: single ip
177
                  (one IP type request is mandatory)
178
    @rtype: (integer, ...)
179
    @return: ((status, answer) or (success, [(status, answer)...])
180

  
181
    """
182
    if isinstance(query, dict):
183
      if constants.CONFD_REQQ_IP in query:
184
        instances_list = [query[constants.CONFD_REQQ_IP]]
185
        mode = constants.CONFD_REQQ_IP
186
      elif constants.CONFD_REQQ_IPLIST in query:
187
        instances_list = query[constants.CONFD_REQQ_IPLIST]
188
        mode = constants.CONFD_REQQ_IPLIST
189
      else:
190
        logging.debug("missing IP or IPLIST in query dict")
191
        return QUERY_ARGUMENT_ERROR
192

  
193
      if constants.CONFD_REQQ_LINK in query:
194
        network_link = query[constants.CONFD_REQQ_LINK]
195
      else:
196
        network_link = None # default will be used
197
    else:
198
      logging.debug("Invalid query argument type for: %s", query)
199
      return QUERY_ARGUMENT_ERROR
200

  
201
    pnodes_list = []
202

  
203
    for instance_ip in instances_list:
204
      if not isinstance(instance_ip, basestring):
205
        logging.debug("Invalid IP type for: %s", instance_ip)
206
        return QUERY_ARGUMENT_ERROR
207

  
208
      instance = self.reader.GetInstanceByLinkIp(instance_ip, network_link)
209
      if not instance:
210
        logging.debug("Unknown instance IP: %s", instance_ip)
211
        pnodes_list.append(QUERY_UNKNOWN_ENTRY_ERROR)
212
        continue
213

  
214
      pnode = self.reader.GetInstancePrimaryNode(instance)
215
      if not pnode:
216
        logging.error("Instance '%s' doesn't have an associated primary"
217
                      " node", instance)
218
        pnodes_list.append(QUERY_INTERNAL_ERROR)
219
        continue
220

  
221
      pnode_primary_ip = self.reader.GetNodePrimaryIp(pnode)
222
      if not pnode_primary_ip:
223
        logging.error("Primary node '%s' doesn't have an associated"
224
                      " primary IP", pnode)
225
        pnodes_list.append(QUERY_INTERNAL_ERROR)
226
        continue
227

  
228
      pnodes_list.append((constants.CONFD_REPL_STATUS_OK, pnode_primary_ip))
229

  
230
    # If a single ip was requested, return a single answer, otherwise
231
    # the whole list, with a success status (since each entry has its
232
    # own success/failure)
233
    if mode == constants.CONFD_REQQ_IP:
234
      return pnodes_list[0]
235

  
236
    return constants.CONFD_REPL_STATUS_OK, pnodes_list
237

  
238

  
239
class NodesPipsQuery(ConfdQuery):
240
  """A query for nodes primary IPs.
241

  
242
  It returns the list of nodes primary IPs.
243

  
244
  """
245
  def Exec(self, query):
246
    """NodesPipsQuery main execution.
247

  
248
    """
249
    if query is None:
250
      status = constants.CONFD_REPL_STATUS_OK
251
      answer = self.reader.GetNodesPrimaryIps()
252
    else:
253
      status = constants.CONFD_REPL_STATUS_ERROR
254
      answer = "non-empty node primary IPs query"
255

  
256
    return status, answer
257

  
258

  
259
class MasterCandidatesPipsQuery(ConfdQuery):
260
  """A query for master candidates primary IPs.
261

  
262
  It returns the list of master candidates primary IPs.
263

  
264
  """
265
  def Exec(self, query):
266
    """MasterCandidatesPipsQuery main execution.
267

  
268
    """
269
    if query is None:
270
      status = constants.CONFD_REPL_STATUS_OK
271
      answer = self.reader.GetMasterCandidatesPrimaryIps()
272
    else:
273
      status = constants.CONFD_REPL_STATUS_ERROR
274
      answer = "non-empty master candidates primary IPs query"
275

  
276
    return status, answer
277

  
278

  
279
class InstancesIpsQuery(ConfdQuery):
280
  """A query for instances IPs.
281

  
282
  It returns the list of IPs of NICs connected to the requested link or all the
283
  instances IPs if no link is submitted.
284

  
285
  """
286
  def Exec(self, query):
287
    """InstancesIpsQuery main execution.
288

  
289
    """
290
    link = query
291
    status = constants.CONFD_REPL_STATUS_OK
292
    answer = self.reader.GetInstancesIps(link)
293

  
294
    return status, answer
295

  
296

  
297
class NodeDrbdQuery(ConfdQuery):
298
  """A query for node drbd minors.
299

  
300
  This is not implemented in the Python confd.
301

  
302
  """
/dev/null
1
#
2
#
3

  
4
# Copyright (C) 2009, 2012 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 server library.
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 logging
30
import time
31

  
32
from ganeti import constants
33
from ganeti import objects
34
from ganeti import errors
35
from ganeti import utils
36
from ganeti import serializer
37
from ganeti import ssconf
38
from ganeti import pathutils
39

  
40
from ganeti.confd import querylib
41

  
42

  
43
class ConfdProcessor(object):
44
  """A processor for confd requests.
45

  
46
  @ivar reader: confd SimpleConfigReader
47
  @ivar disabled: whether confd serving is disabled
48

  
49
  """
50
  DISPATCH_TABLE = {
51
    constants.CONFD_REQ_PING: querylib.PingQuery,
52
    constants.CONFD_REQ_NODE_ROLE_BYNAME: querylib.NodeRoleQuery,
53
    constants.CONFD_REQ_NODE_PIP_BY_INSTANCE_IP:
54
      querylib.InstanceIpToNodePrimaryIpQuery,
55
    constants.CONFD_REQ_CLUSTER_MASTER: querylib.ClusterMasterQuery,
56
    constants.CONFD_REQ_NODE_PIP_LIST: querylib.NodesPipsQuery,
57
    constants.CONFD_REQ_MC_PIP_LIST: querylib.MasterCandidatesPipsQuery,
58
    constants.CONFD_REQ_INSTANCES_IPS_LIST: querylib.InstancesIpsQuery,
59
    constants.CONFD_REQ_NODE_DRBD: querylib.NodeDrbdQuery,
60
    }
61

  
62
  def __init__(self):
63
    """Constructor for ConfdProcessor
64

  
65
    """
66
    self.disabled = True
67
    self.hmac_key = utils.ReadFile(pathutils.CONFD_HMAC_KEY)
68
    self.reader = None
69
    assert \
70
      not constants.CONFD_REQS.symmetric_difference(self.DISPATCH_TABLE), \
71
      "DISPATCH_TABLE is unaligned with CONFD_REQS"
72

  
73
  def Enable(self):
74
    try:
75
      self.reader = ssconf.SimpleConfigReader()
76
      self.disabled = False
77
    except errors.ConfigurationError:
78
      self.disabled = True
79
      raise
80

  
81
  def Disable(self):
82
    self.disabled = True
83
    self.reader = None
84

  
85
  def ExecQuery(self, payload_in, ip, port):
86
    """Process a single UDP request from a client.
87

  
88
    @type payload_in: string
89
    @param payload_in: request raw data
90
    @type ip: string
91
    @param ip: source ip address
92
    @param port: integer
93
    @type port: source port
94

  
95
    """
96
    if self.disabled:
97
      logging.debug("Confd is disabled. Ignoring query.")
98
      return
99
    try:
100
      request = self.ExtractRequest(payload_in)
101
      reply, rsalt = self.ProcessRequest(request)
102
      payload_out = self.PackReply(reply, rsalt)
103
      return payload_out
104
    except errors.ConfdRequestError, err:
105
      logging.info("Ignoring broken query from %s:%d: %s", ip, port, err)
106
      return None
107

  
108
  def ExtractRequest(self, payload):
109
    """Extracts a ConfdRequest object from a serialized hmac signed string.
110

  
111
    This functions also performs signature/timestamp validation.
112

  
113
    """
114
    current_time = time.time()
115
    logging.debug("Extracting request with size: %d", len(payload))
116
    try:
117
      (message, salt) = serializer.LoadSigned(payload, self.hmac_key)
118
    except errors.SignatureError, err:
119
      msg = "invalid signature: %s" % err
120
      raise errors.ConfdRequestError(msg)
121
    try:
122
      message_timestamp = int(salt)
123
    except (ValueError, TypeError):
124
      msg = "non-integer timestamp: %s" % salt
125
      raise errors.ConfdRequestError(msg)
126

  
127
    skew = abs(current_time - message_timestamp)
128
    if skew > constants.CONFD_MAX_CLOCK_SKEW:
129
      msg = "outside time range (skew: %d)" % skew
130
      raise errors.ConfdRequestError(msg)
131

  
132
    try:
133
      request = objects.ConfdRequest.FromDict(message)
134
    except AttributeError, err:
135
      raise errors.ConfdRequestError(str(err))
136

  
137
    return request
138

  
139
  def ProcessRequest(self, request):
140
    """Process one ConfdRequest request, and produce an answer
141

  
142
    @type request: L{objects.ConfdRequest}
143
    @rtype: (L{objects.ConfdReply}, string)
144
    @return: tuple of reply and salt to add to the signature
145

  
146
    """
147
    logging.debug("Processing request: %s", request)
148
    if request.protocol != constants.CONFD_PROTOCOL_VERSION:
149
      msg = "wrong protocol version %d" % request.protocol
150
      raise errors.ConfdRequestError(msg)
151

  
152
    if request.type not in constants.CONFD_REQS:
153
      msg = "wrong request type %d" % request.type
154
      raise errors.ConfdRequestError(msg)
155

  
156
    rsalt = request.rsalt
157
    if not rsalt:
158
      msg = "missing requested salt"
159
      raise errors.ConfdRequestError(msg)
160

  
161
    query_object = self.DISPATCH_TABLE[request.type](self.reader)
162
    status, answer = query_object.Exec(request.query)
163
    reply = objects.ConfdReply(
164
              protocol=constants.CONFD_PROTOCOL_VERSION,
165
              status=status,
166
              answer=answer,
167
              serial=self.reader.GetConfigSerialNo(),
168
              )
169

  
170
    logging.debug("Sending reply: %s", reply)
171

  
172
    return (reply, rsalt)
173

  
174
  def PackReply(self, reply, rsalt):
175
    """Serialize and sign the given reply, with salt rsalt
176

  
177
    @type reply: L{objects.ConfdReply}
178
    @type rsalt: string
179

  
180
    """
181
    return serializer.DumpSigned(reply.ToDict(), self.hmac_key, rsalt)
/dev/null
1
#
2
#
3

  
4
# Copyright (C) 2009, 2010 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
# pylint: disable=C0103
30
# C0103: Invalid name ganeti-confd
31

  
32
import os
33
import sys
34
import logging
35
import time
36

  
37
try:
38
  # pylint: disable=E0611
39
  from pyinotify import pyinotify
40
except ImportError:
41
  import pyinotify
42

  
43
from optparse import OptionParser
44

  
45
from ganeti import asyncnotifier
46
from ganeti import confd
47
from ganeti.confd import server as confd_server
48
from ganeti import constants
49
from ganeti import errors
50
from ganeti import daemon
51
from ganeti import netutils
52
from ganeti import pathutils
53

  
54

  
55
class ConfdAsyncUDPServer(daemon.AsyncUDPSocket):
56
  """The confd udp server, suitable for use with asyncore.
57

  
58
  """
59
  def __init__(self, bind_address, port, processor):
60
    """Constructor for ConfdAsyncUDPServer
61

  
62
    @type bind_address: string
63
    @param bind_address: socket bind address
64
    @type port: int
65
    @param port: udp port
66
    @type processor: L{confd.server.ConfdProcessor}
67
    @param processor: ConfdProcessor to use to handle queries
68

  
69
    """
70
    family = netutils.IPAddress.GetAddressFamily(bind_address)
71
    daemon.AsyncUDPSocket.__init__(self, family)
72
    self.bind_address = bind_address
73
    self.port = port
74
    self.processor = processor
75
    self.bind((bind_address, port))
76
    logging.debug("listening on ('%s':%d)", bind_address, port)
77

  
78
  # this method is overriding a daemon.AsyncUDPSocket method
79
  def handle_datagram(self, payload_in, ip, port):
80
    try:
81
      query = confd.UnpackMagic(payload_in)
82
    except errors.ConfdMagicError, err:
83
      logging.debug(err)
84
      return
85

  
86
    answer = self.processor.ExecQuery(query, ip, port)
87
    if answer is not None:
88
      try:
89
        self.enqueue_send(ip, port, confd.PackMagic(answer))
90
      except errors.UdpDataSizeError:
91
        logging.error("Reply too big to fit in an udp packet.")
92

  
93

  
94
class ConfdConfigurationReloader(object):
95
  """Logic to control when to reload the ganeti configuration
96

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

  
101
  """
102
  def __init__(self, processor, mainloop):
103
    """Constructor for ConfdConfigurationReloader
104

  
105
    @type processor: L{confd.server.ConfdProcessor}
106
    @param processor: ganeti-confd ConfdProcessor
107
    @type mainloop: L{daemon.Mainloop}
108
    @param mainloop: ganeti-confd mainloop
109

  
110
    """
111
    self.processor = processor
112
    self.mainloop = mainloop
113

  
114
    self.polling = True
115
    self.last_notification = 0
116

  
117
    # Asyncronous inotify handler for config changes
118
    cfg_file = pathutils.CLUSTER_CONF_FILE
119
    self.wm = pyinotify.WatchManager()
120
    self.inotify_handler = asyncnotifier.SingleFileEventHandler(self.wm,
121
                                                                self.OnInotify,
122
                                                                cfg_file)
123
    notifier_class = asyncnotifier.ErrorLoggingAsyncNotifier
124
    self.notifier = notifier_class(self.wm, self.inotify_handler)
125

  
126
    self.timer_handle = None
127
    self._EnableTimer()
128

  
129
  def OnInotify(self, notifier_enabled):
130
    """Receive an inotify notification.
131

  
132
    @type notifier_enabled: boolean
133
    @param notifier_enabled: whether the notifier is still enabled
134

  
135
    """
136
    current_time = time.time()
137
    time_delta = current_time - self.last_notification
138
    self.last_notification = current_time
139

  
140
    if time_delta < constants.CONFD_CONFIG_RELOAD_RATELIMIT:
141
      logging.debug("Moving from inotify mode to polling mode")
142
      self.polling = True
143
      if notifier_enabled:
144
        self.inotify_handler.disable()
145

  
146
    if not self.polling and not notifier_enabled:
147
      try:
148
        self.inotify_handler.enable()
149
      except errors.InotifyError:
150
        self.polling = True
151

  
152
    try:
153
      reloaded = self.processor.reader.Reload()
154
      if reloaded:
155
        logging.info("Reloaded ganeti config")
156
      else:
157
        logging.debug("Skipped double config reload")
158
    except errors.ConfigurationError:
159
      self.DisableConfd()
160
      self.inotify_handler.disable()
161
      return
162

  
163
    # Reset the timer. If we're polling it will go to the polling rate, if
164
    # we're not it will delay it again to its base safe timeout.
165
    self._ResetTimer()
166

  
167
  def _DisableTimer(self):
168
    if self.timer_handle is not None:
169
      self.mainloop.scheduler.cancel(self.timer_handle)
170
      self.timer_handle = None
171

  
172
  def _EnableTimer(self):
173
    if self.polling:
174
      timeout = constants.CONFD_CONFIG_RELOAD_RATELIMIT
175
    else:
176
      timeout = constants.CONFD_CONFIG_RELOAD_TIMEOUT
177

  
178
    if self.timer_handle is None:
179
      self.timer_handle = self.mainloop.scheduler.enter(
180
        timeout, 1, self.OnTimer, [])
181

  
182
  def _ResetTimer(self):
183
    self._DisableTimer()
184
    self._EnableTimer()
185

  
186
  def OnTimer(self):
187
    """Function called when the timer fires
188

  
189
    """
190
    self.timer_handle = None
191
    reloaded = False
192
    was_disabled = False
193
    try:
194
      if self.processor.reader is None:
195
        was_disabled = True
196
        self.EnableConfd()
197
        reloaded = True
198
      else:
199
        reloaded = self.processor.reader.Reload()
200
    except errors.ConfigurationError:
201
      self.DisableConfd(silent=was_disabled)
202
      return
203

  
204
    if self.polling and reloaded:
205
      logging.info("Reloaded ganeti config")
206
    elif reloaded:
207
      # We have reloaded the config files, but received no inotify event.  If
208
      # an event is pending though, we just happen to have timed out before
209
      # receiving it, so this is not a problem, and we shouldn't alert
210
      if not self.notifier.check_events() and not was_disabled:
211
        logging.warning("Config file reload at timeout (inotify failure)")
212
    elif self.polling:
213
      # We're polling, but we haven't reloaded the config:
214
      # Going back to inotify mode
215
      logging.debug("Moving from polling mode to inotify mode")
216
      self.polling = False
217
      try:
218
        self.inotify_handler.enable()
219
      except errors.InotifyError:
220
        self.polling = True
221
    else:
222
      logging.debug("Performed configuration check")
223

  
224
    self._EnableTimer()
225

  
226
  def DisableConfd(self, silent=False):
227
    """Puts confd in non-serving mode
228

  
229
    """
230
    if not silent:
231
      logging.warning("Confd is being disabled")
232
    self.processor.Disable()
233
    self.polling = False
234
    self._ResetTimer()
235

  
236
  def EnableConfd(self):
237
    self.processor.Enable()
238
    logging.warning("Confd is being enabled")
239
    self.polling = True
240
    self._ResetTimer()
241

  
242

  
243
def CheckConfd(_, args):
244
  """Initial checks whether to run exit with a failure.
245

  
246
  """
247
  if args: # confd doesn't take any arguments
248
    print >> sys.stderr, ("Usage: %s [-f] [-d] [-b ADDRESS]" % sys.argv[0])
249
    sys.exit(constants.EXIT_FAILURE)
250

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

  
257
  # TODO: once we have a cluster param specifying the address family
258
  # preference, we need to check if the requested options.bind_address does not
259
  # conflict with that. If so, we might warn or EXIT_FAILURE.
260

  
261

  
262
def PrepConfd(options, _):
263
  """Prep confd function, executed with PID file held
264

  
265
  """
266
  # TODO: clarify how the server and reloader variables work (they are
267
  # not used)
268

  
269
  # pylint: disable=W0612
270
  mainloop = daemon.Mainloop()
271

  
272
  # Asyncronous confd UDP server
273
  processor = confd_server.ConfdProcessor()
274
  try:
275
    processor.Enable()
276
  except errors.ConfigurationError:
277
    # If enabling the processor has failed, we can still go on, but confd will
278
    # be disabled
279
    logging.warning("Confd is starting in disabled mode")
280

  
281
  server = ConfdAsyncUDPServer(options.bind_address, options.port, processor)
282

  
283
  # Configuration reloader
284
  reloader = ConfdConfigurationReloader(processor, mainloop)
285

  
286
  return mainloop
287

  
288

  
289
def ExecConfd(options, args, prep_data): # pylint: disable=W0613
290
  """Main confd function, executed with PID file held
291

  
292
  """
293
  mainloop = prep_data
294
  mainloop.Run()
295

  
296

  
297
def Main():
298
  """Main function for the confd daemon.
299

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

  
306
  daemon.GenericMain(constants.CONFD, parser, CheckConfd, PrepConfd, ExecConfd)

Also available in: Unified diff