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