From 4ef0399b755ea91f2cae00aaac2663b22f5c3dfc Mon Sep 17 00:00:00 2001 From: Iustin Pop <iustin@google.com> Date: Tue, 22 Nov 2011 11:47:38 +0100 Subject: [PATCH] Add a small confd client This can be used to test live servers; currently there's not direct way to interact with a confd server, except for burnin's builtin tests (which were the source of this file). Signed-off-by: Iustin Pop <iustin@google.com> Reviewed-by: Guido Trotter <ultrotter@google.com> --- Makefile.am | 1 + tools/confd-client | 278 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 279 insertions(+) create mode 100755 tools/confd-client diff --git a/Makefile.am b/Makefile.am index 171e9dd12..05a31521c 100644 --- a/Makefile.am +++ b/Makefile.am @@ -528,6 +528,7 @@ dist_tools_PYTHON = \ tools/cfgupgrade \ tools/cfgupgrade12 \ tools/cluster-merge \ + tools/confd-client \ tools/lvmstrap \ tools/move-instance \ tools/ovfconverter \ diff --git a/tools/confd-client b/tools/confd-client new file mode 100755 index 000000000..285fb72d1 --- /dev/null +++ b/tools/confd-client @@ -0,0 +1,278 @@ +#!/usr/bin/python +# + +# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011 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. + +# pylint: disable=C0103 + +"""confd client program + +This is can be used to test and debug confd daemon functionality. + +""" + +import sys +import optparse +import time + +from ganeti import constants +from ganeti import cli +from ganeti import utils + +from ganeti.confd import client as confd_client + +USAGE = ("\tconfd-client [--addr=host] [--hmac=key]") + +LOG_HEADERS = { + 0: "- ", + 1: "* ", + 2: "" + } + +OPTIONS = [ + cli.cli_option("--hmac", dest="hmac", default=None, + help="Specify HMAC key instead of reading" + " it from the filesystem", + metavar="<KEY>"), + cli.cli_option("-a", "--address", dest="mc", default="localhost", + help="Server IP to query (default: 127.0.0.1)", + metavar="<ADDRESS>"), + cli.cli_option("-r", "--requests", dest="requests", default=100, + help="Number of requests for the timing tests", + type="int", metavar="<REQUESTS>"), + ] + +def Log(msg, *args, **kwargs): + """Simple function that prints out its argument. + + """ + if args: + msg = msg % args + indent = kwargs.get("indent", 0) + sys.stdout.write("%*s%s%s\n" % (2 * indent, "", + LOG_HEADERS.get(indent, " "), msg)) + sys.stdout.flush() + + +def LogAtMost(msgs, count, **kwargs): + """Log at most count of given messages. + + """ + for m in msgs[:count]: + Log(m, **kwargs) + if len(msgs) > count: + Log("...", **kwargs) + + +def Err(msg, exit_code=1): + """Simple error logging that prints to stderr. + + """ + sys.stderr.write(msg + "\n") + sys.stderr.flush() + sys.exit(exit_code) + + +def Usage(): + """Shows program usage information and exits the program.""" + + print >> sys.stderr, "Usage:" + print >> sys.stderr, USAGE + sys.exit(2) + + +class TestClient(object): + """Confd test client.""" + + def __init__(self): + """Constructor.""" + self.opts = None + self.cluster_master = None + self.instance_ips = None + self.is_timing = False + self.ParseOptions() + + def ParseOptions(self): + """Parses the command line options. + + In case of command line errors, it will show the usage and exit the + program. + + """ + parser = optparse.OptionParser(usage="\n%s" % USAGE, + version=("%%prog (ganeti) %s" % + constants.RELEASE_VERSION), + option_list=OPTIONS) + + options, args = parser.parse_args() + if args: + Usage() + + if options.hmac is None: + options.hmac = utils.ReadFile(constants.CONFD_HMAC_KEY) + self.hmac_key = options.hmac + + self.mc_list = [options.mc] + + self.opts = options + + def ConfdCallback(self, reply): + """Callback for confd queries""" + if reply.type == confd_client.UPCALL_REPLY: + answer = reply.server_reply.answer + reqtype = reply.orig_request.type + if reply.server_reply.status != constants.CONFD_REPL_STATUS_OK: + Log("Query %s gave non-ok status %s: %s" % (reply.orig_request, + reply.server_reply.status, + reply.server_reply)) + if self.is_timing: + Err("Aborting timing tests") + if reqtype == constants.CONFD_REQ_CLUSTER_MASTER: + Err("Cannot continue after master query failure") + if reqtype == constants.CONFD_REQ_INSTANCES_IPS_LIST: + Err("Cannot continue after instance IP list query failure") + return + if self.is_timing: + return + if reqtype == constants.CONFD_REQ_PING: + Log("Ping: OK") + elif reqtype == constants.CONFD_REQ_CLUSTER_MASTER: + Log("Master: OK (%s)", answer) + if self.cluster_master is None: + # only assign the first time, in the plain query + self.cluster_master = answer + elif reqtype == constants.CONFD_REQ_NODE_ROLE_BYNAME: + if answer == constants.CONFD_NODE_ROLE_MASTER: + Log("Node role for master: OK",) + else: + Err("Node role for master: wrong: %s" % answer) + elif reqtype == constants.CONFD_REQ_NODE_PIP_LIST: + Log("Node primary ip query: OK") + LogAtMost(answer, 5, indent=1) + elif reqtype == constants.CONFD_REQ_MC_PIP_LIST: + Log("Master candidates primary IP query: OK") + LogAtMost(answer, 5, indent=1) + elif reqtype == constants.CONFD_REQ_INSTANCES_IPS_LIST: + Log("Instance primary IP query: OK") + if not answer: + Log("no IPs received", indent=1) + else: + LogAtMost(answer, 5, indent=1) + self.instance_ips = answer + elif reqtype == constants.CONFD_REQ_NODE_PIP_BY_INSTANCE_IP: + Log("Instance IP to node IP query: OK") + if not answer: + Log("no mapping received", indent=1) + else: + LogAtMost(answer, 5, indent=1) + else: + Log("Unhandled reply %s, please fix the client", reqtype) + print answer + + def DoConfdRequestReply(self, req): + self.confd_counting_callback.RegisterQuery(req.rsalt) + self.confd_client.SendRequest(req, async=False) + while not self.confd_counting_callback.AllAnswered(): + if not self.confd_client.ReceiveReply(): + Err("Did not receive all expected confd replies") + break + + def TestConfd(self): + """Run confd queries for the cluster. + + """ + Log("Checking confd results") + + filter_callback = confd_client.ConfdFilterCallback(self.ConfdCallback) + counting_callback = confd_client.ConfdCountingCallback(filter_callback) + self.confd_counting_callback = counting_callback + + self.confd_client = confd_client.ConfdClient(self.hmac_key, + self.mc_list, + counting_callback) + + tests = [ + {"type": constants.CONFD_REQ_PING }, + {"type": constants.CONFD_REQ_CLUSTER_MASTER }, + {"type": constants.CONFD_REQ_CLUSTER_MASTER, + "query": {constants.CONFD_REQQ_FIELDS : [ + constants.CONFD_REQFIELD_NAME, + constants.CONFD_REQFIELD_IP, + constants.CONFD_REQFIELD_MNODE_PIP, + ]}}, + {"type": constants.CONFD_REQ_NODE_ROLE_BYNAME }, + {"type": constants.CONFD_REQ_NODE_PIP_LIST }, + {"type": constants.CONFD_REQ_MC_PIP_LIST }, + {"type": constants.CONFD_REQ_INSTANCES_IPS_LIST, + "query": None}, + {"type": constants.CONFD_REQ_NODE_PIP_BY_INSTANCE_IP }, + ] + + for kwargs in tests: + if kwargs["type"] == constants.CONFD_REQ_NODE_ROLE_BYNAME: + assert self.cluster_master is not None + kwargs["query"] = self.cluster_master + elif kwargs["type"] == constants.CONFD_REQ_NODE_PIP_BY_INSTANCE_IP: + kwargs["query"] = { constants.CONFD_REQQ_IPLIST : self.instance_ips } + + # pylint: disable=W0142 + # used ** magic + req = confd_client.ConfdClientRequest(**kwargs) + self.DoConfdRequestReply(req) + + def TestTiming(self): + """Run timing tests. + + """ + # timing tests + if self.opts.requests <= 0: + return + Log("Timing tests") + self.is_timing = True + self.TimingOp("ping", {"type": constants.CONFD_REQ_PING}) + self.TimingOp("instance ips", + {"type": constants.CONFD_REQ_INSTANCES_IPS_LIST}) + + def TimingOp(self, name, kwargs): + """Run a single timing test. + + """ + start = time.time() + for i in range(self.opts.requests): + req = confd_client.ConfdClientRequest(**kwargs) + self.DoConfdRequestReply(req) + stop = time.time() + per_req = 1000 * (stop-start) / self.opts.requests + Log("%.3fms per %s request", per_req, name, indent=1) + + def Run(self): + """Run all the tests. + + """ + self.TestConfd() + self.TestTiming() + +def main(): + """Main function. + + """ + return TestClient().Run() + + +if __name__ == "__main__": + main() -- GitLab