Commit b0dcdc10 authored by Iustin Pop's avatar Iustin Pop

Remove the python confd server side code

In 2.7 we will only support the Haskell version, if enabled.

Since the original hconfd enabling was a bit hack-ish (copying over
the actual installed ganeti-confd, Python version), the Makefile.am
changes are a bit more involved than just the removal of the Python
code.
Signed-off-by: default avatarIustin Pop <iustin@google.com>
Reviewed-by: default avatarBernardo Dal Seno <bdalseno@google.com>
parent 04520998
......@@ -41,7 +41,6 @@
/daemons/daemon-util
/daemons/ganeti-cleaner
/daemons/ganeti-master-cleaner
/daemons/ganeti-confd
/daemons/ganeti-masterd
/daemons/ganeti-noded
/daemons/ganeti-rapi
......
......@@ -169,6 +169,7 @@ CLEANFILES = \
$(nodist_pkgpython_PYTHON) \
$(HS_ALL_PROGS) $(HS_BUILT_SRCS) \
$(HS_BUILT_TEST_HELPERS) \
htools/ganeti-confd \
.hpc/*.mix htools/*.tix htest/*.tix \
doc/hs-lint.html
......@@ -292,9 +293,7 @@ http_PYTHON = \
confd_PYTHON = \
lib/confd/__init__.py \
lib/confd/client.py \
lib/confd/querylib.py \
lib/confd/server.py
lib/confd/client.py
masterd_PYTHON = \
lib/masterd/__init__.py \
......@@ -311,7 +310,6 @@ watcher_PYTHON = \
server_PYTHON = \
lib/server/__init__.py \
lib/server/confd.py \
lib/server/masterd.py \
lib/server/noded.py \
lib/server/rapi.py
......@@ -600,9 +598,6 @@ install-exec-hook:
$(LN_S) -f htools \
$(DESTDIR)$(bindir)/$$role ; \
done
if ENABLE_CONFD
mv $(DESTDIR)$(sbindir)/hconfd $(DESTDIR)$(sbindir)/ganeti-confd
endif
endif
$(HS_ALL_PROGS): %: %.hs $(HS_LIBTEST_SRCS) $(HS_BUILT_SRCS) Makefile
......@@ -654,7 +649,10 @@ nodist_sbin_SCRIPTS = \
daemons/ganeti-master-cleaner
if ENABLE_CONFD
nodist_sbin_SCRIPTS += htools/hconfd
htools/ganeti-confd: htools/hconfd
cp -f $< $@
nodist_sbin_SCRIPTS += htools/ganeti-confd
endif
python_scripts = \
......
#
#
# Copyright (C) 2009, 2012 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 queries library.
"""
import logging
from ganeti import constants
# constants for some common errors to return from a query
QUERY_UNKNOWN_ENTRY_ERROR = (constants.CONFD_REPL_STATUS_ERROR,
constants.CONFD_ERROR_UNKNOWN_ENTRY)
QUERY_INTERNAL_ERROR = (constants.CONFD_REPL_STATUS_ERROR,
constants.CONFD_ERROR_INTERNAL)
QUERY_ARGUMENT_ERROR = (constants.CONFD_REPL_STATUS_ERROR,
constants.CONFD_ERROR_ARGUMENT)
class ConfdQuery(object):
"""Confd Query base class.
"""
def __init__(self, reader):
"""Constructor for Confd Query
@type reader: L{ssconf.SimpleConfigReader}
@param reader: ConfigReader to use to access the config
"""
self.reader = reader
def Exec(self, query): # pylint: disable=R0201,W0613
"""Process a single UDP request from a client.
Different queries should override this function, which by defaults returns
a "non-implemented" answer.
@type query: (undefined)
@param query: ConfdRequest 'query' field
@rtype: (integer, undefined)
@return: status and answer to give to the client
"""
status = constants.CONFD_REPL_STATUS_NOTIMPLEMENTED
answer = "not implemented"
return status, answer
class PingQuery(ConfdQuery):
"""An empty confd query.
It will return success on an empty argument, and an error on any other
argument.
"""
def Exec(self, query):
"""PingQuery main execution.
"""
if query is None:
status = constants.CONFD_REPL_STATUS_OK
answer = "ok"
else:
status = constants.CONFD_REPL_STATUS_ERROR
answer = "non-empty ping query"
return status, answer
class ClusterMasterQuery(ConfdQuery):
"""Cluster master query.
It accepts no arguments, and returns the current cluster master.
"""
def _GetMasterNode(self):
return self.reader.GetMasterNode()
def Exec(self, query):
"""ClusterMasterQuery main execution
"""
if isinstance(query, dict):
if constants.CONFD_REQQ_FIELDS in query:
status = constants.CONFD_REPL_STATUS_OK
req_fields = query[constants.CONFD_REQQ_FIELDS]
if not isinstance(req_fields, (list, tuple)):
logging.debug("FIELDS request should be a list")
return QUERY_ARGUMENT_ERROR
answer = []
for field in req_fields:
if field == constants.CONFD_REQFIELD_NAME:
answer.append(self._GetMasterNode())
elif field == constants.CONFD_REQFIELD_IP:
answer.append(self.reader.GetMasterIP())
elif field == constants.CONFD_REQFIELD_MNODE_PIP:
answer.append(self.reader.GetNodePrimaryIp(self._GetMasterNode()))
else:
logging.debug("missing FIELDS in query dict")
return QUERY_ARGUMENT_ERROR
elif not query:
status = constants.CONFD_REPL_STATUS_OK
answer = self.reader.GetMasterNode()
else:
logging.debug("Invalid master query argument: not dict or empty")
return QUERY_ARGUMENT_ERROR
return status, answer
class NodeRoleQuery(ConfdQuery):
"""A query for the role of a node.
It will return one of CONFD_NODE_ROLE_*, or an error for non-existing nodes.
"""
def Exec(self, query):
"""EmptyQuery main execution
"""
node = query
if self.reader.GetMasterNode() == node:
status = constants.CONFD_REPL_STATUS_OK
answer = constants.CONFD_NODE_ROLE_MASTER
return status, answer
flags = self.reader.GetNodeStatusFlags(node)
if flags is None:
return QUERY_UNKNOWN_ENTRY_ERROR
master_candidate, drained, offline = flags
if master_candidate:
answer = constants.CONFD_NODE_ROLE_CANDIDATE
elif drained:
answer = constants.CONFD_NODE_ROLE_DRAINED
elif offline:
answer = constants.CONFD_NODE_ROLE_OFFLINE
else:
answer = constants.CONFD_NODE_ROLE_REGULAR
return constants.CONFD_REPL_STATUS_OK, answer
class InstanceIpToNodePrimaryIpQuery(ConfdQuery):
"""A query for the location of one or more instance's ips.
"""
def Exec(self, query):
"""InstanceIpToNodePrimaryIpQuery main execution.
@type query: string or dict
@param query: instance ip or dict containing:
constants.CONFD_REQQ_LINK: nic link (optional)
constants.CONFD_REQQ_IPLIST: list of ips
constants.CONFD_REQQ_IP: single ip
(one IP type request is mandatory)
@rtype: (integer, ...)
@return: ((status, answer) or (success, [(status, answer)...])
"""
if isinstance(query, dict):
if constants.CONFD_REQQ_IP in query:
instances_list = [query[constants.CONFD_REQQ_IP]]
mode = constants.CONFD_REQQ_IP
elif constants.CONFD_REQQ_IPLIST in query:
instances_list = query[constants.CONFD_REQQ_IPLIST]
mode = constants.CONFD_REQQ_IPLIST
else:
logging.debug("missing IP or IPLIST in query dict")
return QUERY_ARGUMENT_ERROR
if constants.CONFD_REQQ_LINK in query:
network_link = query[constants.CONFD_REQQ_LINK]
else:
network_link = None # default will be used
else:
logging.debug("Invalid query argument type for: %s", query)
return QUERY_ARGUMENT_ERROR
pnodes_list = []
for instance_ip in instances_list:
if not isinstance(instance_ip, basestring):
logging.debug("Invalid IP type for: %s", instance_ip)
return QUERY_ARGUMENT_ERROR
instance = self.reader.GetInstanceByLinkIp(instance_ip, network_link)
if not instance:
logging.debug("Unknown instance IP: %s", instance_ip)
pnodes_list.append(QUERY_UNKNOWN_ENTRY_ERROR)
continue
pnode = self.reader.GetInstancePrimaryNode(instance)
if not pnode:
logging.error("Instance '%s' doesn't have an associated primary"
" node", instance)
pnodes_list.append(QUERY_INTERNAL_ERROR)
continue
pnode_primary_ip = self.reader.GetNodePrimaryIp(pnode)
if not pnode_primary_ip:
logging.error("Primary node '%s' doesn't have an associated"
" primary IP", pnode)
pnodes_list.append(QUERY_INTERNAL_ERROR)
continue
pnodes_list.append((constants.CONFD_REPL_STATUS_OK, pnode_primary_ip))
# If a single ip was requested, return a single answer, otherwise
# the whole list, with a success status (since each entry has its
# own success/failure)
if mode == constants.CONFD_REQQ_IP:
return pnodes_list[0]
return constants.CONFD_REPL_STATUS_OK, pnodes_list
class NodesPipsQuery(ConfdQuery):
"""A query for nodes primary IPs.
It returns the list of nodes primary IPs.
"""
def Exec(self, query):
"""NodesPipsQuery main execution.
"""
if query is None:
status = constants.CONFD_REPL_STATUS_OK
answer = self.reader.GetNodesPrimaryIps()
else:
status = constants.CONFD_REPL_STATUS_ERROR
answer = "non-empty node primary IPs query"
return status, answer
class MasterCandidatesPipsQuery(ConfdQuery):
"""A query for master candidates primary IPs.
It returns the list of master candidates primary IPs.
"""
def Exec(self, query):
"""MasterCandidatesPipsQuery main execution.
"""
if query is None:
status = constants.CONFD_REPL_STATUS_OK
answer = self.reader.GetMasterCandidatesPrimaryIps()
else:
status = constants.CONFD_REPL_STATUS_ERROR
answer = "non-empty master candidates primary IPs query"
return status, answer
class InstancesIpsQuery(ConfdQuery):
"""A query for instances IPs.
It returns the list of IPs of NICs connected to the requested link or all the
instances IPs if no link is submitted.
"""
def Exec(self, query):
"""InstancesIpsQuery main execution.
"""
link = query
status = constants.CONFD_REPL_STATUS_OK
answer = self.reader.GetInstancesIps(link)
return status, answer
class NodeDrbdQuery(ConfdQuery):
"""A query for node drbd minors.
This is not implemented in the Python confd.
"""
#
#
# Copyright (C) 2009, 2012 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 server library.
Ganeti-confd is a daemon to query master candidates for configuration values.
It uses UDP+HMAC for authentication with a global cluster key.
"""
import logging
import time
from ganeti import constants
from ganeti import objects
from ganeti import errors
from ganeti import utils
from ganeti import serializer
from ganeti import ssconf
from ganeti import pathutils
from ganeti.confd import querylib
class ConfdProcessor(object):
"""A processor for confd requests.
@ivar reader: confd SimpleConfigReader
@ivar disabled: whether confd serving is disabled
"""
DISPATCH_TABLE = {
constants.CONFD_REQ_PING: querylib.PingQuery,
constants.CONFD_REQ_NODE_ROLE_BYNAME: querylib.NodeRoleQuery,
constants.CONFD_REQ_NODE_PIP_BY_INSTANCE_IP:
querylib.InstanceIpToNodePrimaryIpQuery,
constants.CONFD_REQ_CLUSTER_MASTER: querylib.ClusterMasterQuery,
constants.CONFD_REQ_NODE_PIP_LIST: querylib.NodesPipsQuery,
constants.CONFD_REQ_MC_PIP_LIST: querylib.MasterCandidatesPipsQuery,
constants.CONFD_REQ_INSTANCES_IPS_LIST: querylib.InstancesIpsQuery,
constants.CONFD_REQ_NODE_DRBD: querylib.NodeDrbdQuery,
}
def __init__(self):
"""Constructor for ConfdProcessor
"""
self.disabled = True
self.hmac_key = utils.ReadFile(pathutils.CONFD_HMAC_KEY)
self.reader = None
assert \
not constants.CONFD_REQS.symmetric_difference(self.DISPATCH_TABLE), \
"DISPATCH_TABLE is unaligned with CONFD_REQS"
def Enable(self):
try:
self.reader = ssconf.SimpleConfigReader()
self.disabled = False
except errors.ConfigurationError:
self.disabled = True
raise
def Disable(self):
self.disabled = True
self.reader = None
def ExecQuery(self, payload_in, ip, port):
"""Process a single UDP request from a client.
@type payload_in: string
@param payload_in: request raw data
@type ip: string
@param ip: source ip address
@param port: integer
@type port: source port
"""
if self.disabled:
logging.debug("Confd is disabled. Ignoring query.")
return
try:
request = self.ExtractRequest(payload_in)
reply, rsalt = self.ProcessRequest(request)
payload_out = self.PackReply(reply, rsalt)
return payload_out
except errors.ConfdRequestError, err:
logging.info("Ignoring broken query from %s:%d: %s", ip, port, err)
return None
def ExtractRequest(self, payload):
"""Extracts a ConfdRequest object from a serialized hmac signed string.
This functions also performs signature/timestamp validation.
"""
current_time = time.time()
logging.debug("Extracting request with size: %d", len(payload))
try:
(message, salt) = serializer.LoadSigned(payload, self.hmac_key)
except errors.SignatureError, err:
msg = "invalid signature: %s" % err
raise errors.ConfdRequestError(msg)
try:
message_timestamp = int(salt)
except (ValueError, TypeError):
msg = "non-integer timestamp: %s" % salt
raise errors.ConfdRequestError(msg)
skew = abs(current_time - message_timestamp)
if skew > constants.CONFD_MAX_CLOCK_SKEW:
msg = "outside time range (skew: %d)" % skew
raise errors.ConfdRequestError(msg)
try:
request = objects.ConfdRequest.FromDict(message)
except AttributeError, err:
raise errors.ConfdRequestError(str(err))
return request
def ProcessRequest(self, request):
"""Process one ConfdRequest request, and produce an answer
@type request: L{objects.ConfdRequest}
@rtype: (L{objects.ConfdReply}, string)
@return: tuple of reply and salt to add to the signature
"""
logging.debug("Processing request: %s", request)
if request.protocol != constants.CONFD_PROTOCOL_VERSION:
msg = "wrong protocol version %d" % request.protocol
raise errors.ConfdRequestError(msg)
if request.type not in constants.CONFD_REQS:
msg = "wrong request type %d" % request.type
raise errors.ConfdRequestError(msg)
rsalt = request.rsalt
if not rsalt:
msg = "missing requested salt"
raise errors.ConfdRequestError(msg)
query_object = self.DISPATCH_TABLE[request.type](self.reader)
status, answer = query_object.Exec(request.query)
reply = objects.ConfdReply(
protocol=constants.CONFD_PROTOCOL_VERSION,
status=status,
answer=answer,
serial=self.reader.GetConfigSerialNo(),
)
logging.debug("Sending reply: %s", reply)
return (reply, rsalt)
def PackReply(self, reply, rsalt):
"""Serialize and sign the given reply, with salt rsalt
@type reply: L{objects.ConfdReply}
@type rsalt: string
"""
return serializer.DumpSigned(reply.ToDict(), self.hmac_key, rsalt)
#
#
# Copyright (C) 2009, 2010 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.
"""
# pylint: disable=C0103
# C0103: Invalid name ganeti-confd
import os
import sys
import logging
import time
try:
# pylint: disable=E0611
from pyinotify import pyinotify
except ImportError:
import pyinotify
from optparse import OptionParser
from ganeti import asyncnotifier
from ganeti import confd
from ganeti.confd import server as confd_server
from ganeti import constants
from ganeti import errors
from ganeti import daemon
from ganeti import netutils
from ganeti import pathutils
class ConfdAsyncUDPServer(daemon.AsyncUDPSocket):
"""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
@type port: int
@param port: udp port
@type processor: L{confd.server.ConfdProcessor}
@param processor: ConfdProcessor to use to handle queries
"""
family = netutils.IPAddress.GetAddressFamily(bind_address)
daemon.AsyncUDPSocket.__init__(self, family)
self.bind_address = bind_address
self.port = port
self.processor = processor
self.bind((bind_address, port))
logging.debug("listening on ('%s':%d)", bind_address, port)
# this method is overriding a daemon.AsyncUDPSocket method
def handle_datagram(self, payload_in, ip, port):
try:
query = confd.UnpackMagic(payload_in)
except errors.ConfdMagicError, err:
logging.debug(err)
return
answer = self.processor.ExecQuery(query, ip, port)
if answer is not None:
try:
self.enqueue_send(ip, port, confd.PackMagic(answer))
except errors.UdpDataSizeError:
logging.error("Reply too big to fit in an udp packet.")
class ConfdConfigurationReloader(object):
"""Logic to control when to reload the ganeti configuration
This class is able to alter between inotify and polling, to rate-limit the
number of reloads. When using inotify it also supports a fallback timed
check, to verify that the reload hasn't failed.
"""
def __init__(self, processor, mainloop):
"""Constructor for ConfdConfigurationReloader
@type processor: L{confd.server.ConfdProcessor}
@param processor: ganeti-confd ConfdProcessor
@type mainloop: L{daemon.Mainloop}
@param mainloop: ganeti-confd mainloop
"""
self.processor = processor
self.mainloop = mainloop
self.polling = True
self.last_notification = 0
# Asyncronous inotify handler for config changes
cfg_file = pathutils.CLUSTER_CONF_FILE
self.wm = pyinotify.WatchManager()
self.inotify_handler = asyncnotifier.SingleFileEventHandler(self.wm,
self.OnInotify,