Include VCS version in `gnt-cluster version`
[ganeti-local] / tools / confd-client
1 #!/usr/bin/python
2 #
3
4 # Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011 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 # pylint: disable=C0103
22
23 """confd client program
24
25 This is can be used to test and debug confd daemon functionality.
26
27 """
28
29 import sys
30 import optparse
31 import time
32
33 from ganeti import constants
34 from ganeti import cli
35 from ganeti import utils
36 from ganeti import pathutils
37
38 from ganeti.confd import client as confd_client
39
40 USAGE = ("\tconfd-client [--addr=host] [--hmac=key]")
41
42 LOG_HEADERS = {
43   0: "- ",
44   1: "* ",
45   2: "",
46   }
47
48 OPTIONS = [
49   cli.cli_option("--hmac", dest="hmac", default=None,
50                  help="Specify HMAC key instead of reading"
51                  " it from the filesystem",
52                  metavar="<KEY>"),
53   cli.cli_option("-a", "--address", dest="mc", default="localhost",
54                  help="Server IP to query (default: 127.0.0.1)",
55                  metavar="<ADDRESS>"),
56   cli.cli_option("-r", "--requests", dest="requests", default=100,
57                  help="Number of requests for the timing tests",
58                  type="int", metavar="<REQUESTS>"),
59   ]
60
61
62 def Log(msg, *args, **kwargs):
63   """Simple function that prints out its argument.
64
65   """
66   if args:
67     msg = msg % args
68   indent = kwargs.get("indent", 0)
69   sys.stdout.write("%*s%s%s\n" % (2 * indent, "",
70                                   LOG_HEADERS.get(indent, "  "), msg))
71   sys.stdout.flush()
72
73
74 def LogAtMost(msgs, count, **kwargs):
75   """Log at most count of given messages.
76
77   """
78   for m in msgs[:count]:
79     Log(m, **kwargs)
80   if len(msgs) > count:
81     Log("...", **kwargs)
82
83
84 def Err(msg, exit_code=1):
85   """Simple error logging that prints to stderr.
86
87   """
88   sys.stderr.write(msg + "\n")
89   sys.stderr.flush()
90   sys.exit(exit_code)
91
92
93 def Usage():
94   """Shows program usage information and exits the program."""
95
96   print >> sys.stderr, "Usage:"
97   print >> sys.stderr, USAGE
98   sys.exit(2)
99
100
101 class TestClient(object):
102   """Confd test client."""
103
104   def __init__(self):
105     """Constructor."""
106     self.opts = None
107     self.cluster_master = None
108     self.instance_ips = None
109     self.is_timing = False
110     self.ParseOptions()
111
112   def ParseOptions(self):
113     """Parses the command line options.
114
115     In case of command line errors, it will show the usage and exit the
116     program.
117
118     """
119     parser = optparse.OptionParser(usage="\n%s" % USAGE,
120                                    version=("%%prog (ganeti) %s" %
121                                             constants.RELEASE_VERSION),
122                                    option_list=OPTIONS)
123
124     options, args = parser.parse_args()
125     if args:
126       Usage()
127
128     if options.hmac is None:
129       options.hmac = utils.ReadFile(pathutils.CONFD_HMAC_KEY)
130     self.hmac_key = options.hmac
131
132     self.mc_list = [options.mc]
133
134     self.opts = options
135
136   def ConfdCallback(self, reply):
137     """Callback for confd queries"""
138     if reply.type == confd_client.UPCALL_REPLY:
139       answer = reply.server_reply.answer
140       reqtype = reply.orig_request.type
141       if reply.server_reply.status != constants.CONFD_REPL_STATUS_OK:
142         Log("Query %s gave non-ok status %s: %s" % (reply.orig_request,
143                                                     reply.server_reply.status,
144                                                     reply.server_reply))
145         if self.is_timing:
146           Err("Aborting timing tests")
147         if reqtype == constants.CONFD_REQ_CLUSTER_MASTER:
148           Err("Cannot continue after master query failure")
149         if reqtype == constants.CONFD_REQ_INSTANCES_IPS_LIST:
150           Err("Cannot continue after instance IP list query failure")
151         return
152       if self.is_timing:
153         return
154       if reqtype == constants.CONFD_REQ_PING:
155         Log("Ping: OK")
156       elif reqtype == constants.CONFD_REQ_CLUSTER_MASTER:
157         Log("Master: OK (%s)", answer)
158         if self.cluster_master is None:
159           # only assign the first time, in the plain query
160           self.cluster_master = answer
161       elif reqtype == constants.CONFD_REQ_NODE_ROLE_BYNAME:
162         if answer == constants.CONFD_NODE_ROLE_MASTER:
163           Log("Node role for master: OK",)
164         else:
165           Err("Node role for master: wrong: %s" % answer)
166       elif reqtype == constants.CONFD_REQ_NODE_PIP_LIST:
167         Log("Node primary ip query: OK")
168         LogAtMost(answer, 5, indent=1)
169       elif reqtype == constants.CONFD_REQ_MC_PIP_LIST:
170         Log("Master candidates primary IP query: OK")
171         LogAtMost(answer, 5, indent=1)
172       elif reqtype == constants.CONFD_REQ_INSTANCES_IPS_LIST:
173         Log("Instance primary IP query: OK")
174         if not answer:
175           Log("no IPs received", indent=1)
176         else:
177           LogAtMost(answer, 5, indent=1)
178         self.instance_ips = answer
179       elif reqtype == constants.CONFD_REQ_NODE_PIP_BY_INSTANCE_IP:
180         Log("Instance IP to node IP query: OK")
181         if not answer:
182           Log("no mapping received", indent=1)
183         else:
184           LogAtMost(answer, 5, indent=1)
185       else:
186         Log("Unhandled reply %s, please fix the client", reqtype)
187         print answer
188
189   def DoConfdRequestReply(self, req):
190     self.confd_counting_callback.RegisterQuery(req.rsalt)
191     self.confd_client.SendRequest(req, async=False)
192     while not self.confd_counting_callback.AllAnswered():
193       if not self.confd_client.ReceiveReply():
194         Err("Did not receive all expected confd replies")
195         break
196
197   def TestConfd(self):
198     """Run confd queries for the cluster.
199
200     """
201     Log("Checking confd results")
202
203     filter_callback = confd_client.ConfdFilterCallback(self.ConfdCallback)
204     counting_callback = confd_client.ConfdCountingCallback(filter_callback)
205     self.confd_counting_callback = counting_callback
206
207     self.confd_client = confd_client.ConfdClient(self.hmac_key,
208                                                  self.mc_list,
209                                                  counting_callback)
210
211     tests = [
212       {"type": constants.CONFD_REQ_PING},
213       {"type": constants.CONFD_REQ_CLUSTER_MASTER},
214       {"type": constants.CONFD_REQ_CLUSTER_MASTER,
215        "query": {constants.CONFD_REQQ_FIELDS:
216                  [constants.CONFD_REQFIELD_NAME,
217                   constants.CONFD_REQFIELD_IP,
218                   constants.CONFD_REQFIELD_MNODE_PIP,
219                   ]}},
220       {"type": constants.CONFD_REQ_NODE_ROLE_BYNAME},
221       {"type": constants.CONFD_REQ_NODE_PIP_LIST},
222       {"type": constants.CONFD_REQ_MC_PIP_LIST},
223       {"type": constants.CONFD_REQ_INSTANCES_IPS_LIST,
224        "query": None},
225       {"type": constants.CONFD_REQ_NODE_PIP_BY_INSTANCE_IP},
226       ]
227
228     for kwargs in tests:
229       if kwargs["type"] == constants.CONFD_REQ_NODE_ROLE_BYNAME:
230         assert self.cluster_master is not None
231         kwargs["query"] = self.cluster_master
232       elif kwargs["type"] == constants.CONFD_REQ_NODE_PIP_BY_INSTANCE_IP:
233         kwargs["query"] = {constants.CONFD_REQQ_IPLIST: self.instance_ips}
234
235       # pylint: disable=W0142
236       # used ** magic
237       req = confd_client.ConfdClientRequest(**kwargs)
238       self.DoConfdRequestReply(req)
239
240   def TestTiming(self):
241     """Run timing tests.
242
243     """
244     # timing tests
245     if self.opts.requests <= 0:
246       return
247     Log("Timing tests")
248     self.is_timing = True
249     self.TimingOp("ping", {"type": constants.CONFD_REQ_PING})
250     self.TimingOp("instance ips",
251                   {"type": constants.CONFD_REQ_INSTANCES_IPS_LIST})
252
253   def TimingOp(self, name, kwargs):
254     """Run a single timing test.
255
256     """
257     start = time.time()
258     for _ in range(self.opts.requests):
259       # pylint: disable=W0142
260       req = confd_client.ConfdClientRequest(**kwargs)
261       self.DoConfdRequestReply(req)
262     stop = time.time()
263     per_req = 1000 * (stop - start) / self.opts.requests
264     Log("%.3fms per %s request", per_req, name, indent=1)
265
266   def Run(self):
267     """Run all the tests.
268
269     """
270     self.TestConfd()
271     self.TestTiming()
272
273
274 def main():
275   """Main function.
276
277   """
278   return TestClient().Run()
279
280
281 if __name__ == "__main__":
282   main()