import repository from arizona
[raven.git] / apps / digdug / digdug.py
1 #! /usr/bin/env python
2
3 from ConfigParser import ConfigParser
4 from optparse import OptionParser
5 import os
6 import pwd
7 import socket
8 import sys
9 import threading
10 import time
11
12 import ravenlib.tableprint
13 import ravenlib.report
14 from ravenlib.client.digdugapi import DigDugClient
15
16 DEFAULT_PORT = 4321
17
18 def get_homedir():
19     """ get the current user's home directory"""
20     home = os.path.expanduser("~")
21
22     # When something is run from RPM via Stork, often it doesn't have $HOME
23     # in the environment. So, look up the passwd entry and get the home
24     # directory from there.
25     if (home == ""):
26         pwent = pwd.getpwuid(os.getuid())
27         home = pwent.pw_dir
28
29     return home
30
31 CONFIG_DIR = os.path.join(get_homedir(), ".digdug")
32 CONFIG_FILE = CONFIG_DIR + "/digdug.conf"
33
34 # global variables
35 glo_client = None
36 glo_client_kind = None
37 glo_client_version = None
38 glo_client_release = None
39 glo_config = None
40 glo_options = None
41 glo_command = None
42
43 EXIT_BADARG = -1
44 EXIT_NODIGDUG = -2
45 EXIT_BADVERSION = -3
46 EXIT_NOTSUBSCRIBED = -4
47 EXIT_STALE = -5
48
49 def verbose_exit(code, msg):
50    if msg:
51        ravenlib.report.error(msg)
52    ravenlib.report.error("exit " + str(code))
53    sys.exit(code)
54
55 def verbose_exit_during_connect(code, shortmsg, msg):
56    # an ugly kludge to get an appropriate error message for owl
57    if glo_command == "showowl":
58        print "[digdugclient]"
59        print "status =", shortmsg
60        sys.exit(code)
61
62    verbose_exit(code, msg)
63
64 def create_cmd_parser(command, additional_cmdargs = None):
65    cmdargs = {"clearstate": "state ...",
66               "incversion": "",
67               "purge": "",
68               "setchannel": "name",
69               "setstate": "state ...",
70               "verifyversion": "",
71               "waitfor": "state_name",
72               "subscribe": "host:port channel [channel2 ...]",
73               "show": "",
74               "showchannels": "",
75               "showclients": "",
76               "showowl": "",
77               "setuser": "name value"
78
79              }
80
81    if additional_cmdargs:
82       cmdargs.update(additional_cmdargs)
83
84    if command not in cmdargs:
85       print "Invalid command\n"
86       print "Commands: ",
87       print ",".join(cmdargs.keys())
88       print ""
89       verbose_exit(EXIT_BADARG, "invalid command " + str(command))
90
91    parser = OptionParser(usage="digdug [general_options] %s [cmd_options] %s" \
92       % (command, cmdargs[command]))
93
94    if (command == "waitfor"):
95        parser.add_option("-n", "--newversion", dest="newversion",
96            help="allow version number change", action="store_true", default=False)
97        parser.add_option("-c", "--count", dest="count",
98            help="number of clients to wait for", action="store", type="int", default=1)
99
100    return parser
101
102 def create_parser():
103    # Generate command line parser
104    parser = OptionParser(usage="digdug [cmd_options] <command> [cmd_options]",
105         description="Configures digdug")
106
107    parser.add_option("-a", "--addr", dest="addr",
108         help="address of digdugclient or digdugserver", action="store", type="string", default=None)
109    parser.add_option("-n", "--channel", dest="channel",
110         help="name of channel", action="store", type="string", default=get_config_opt("channel", "my_experiment"))
111    parser.add_option("-p", "--port", dest="port",
112         help="port to listen on", action="store", type="int", default=DEFAULT_PORT)
113    parser.add_option("-s", "--server", dest="server",
114         help="connect to local digdug server instead of local digdug client", action="store_true", default=False)
115    parser.add_option("-v", "--verbose", dest="verbose",
116         help="verbose mode", action="store_true", default=False)
117
118    parser.disable_interspersed_args()
119
120    return parser
121
122 def get_config_opt(name, default):
123     if glo_config.has_option("DEFAULT", name):
124         return glo_config.get("DEFAULT", name)
125     else:
126         return default
127
128 def read_config_file():
129     global glo_config
130
131     if not os.path.isdir(CONFIG_DIR):
132         os.mkdir(CONFIG_DIR)
133
134     glo_config = ConfigParser()
135     if os.path.isfile(CONFIG_FILE):
136         glo_config.read(CONFIG_FILE)
137
138 def save_config_file():
139     global glo_config
140
141     glo_config.write(file(CONFIG_FILE,"w"))
142
143 def show_channel(name):
144     dict = glo_client.get_channel(glo_options.channel)
145
146     print "channel status for", glo_options.channel
147
148     print "  version:", dict.get("version", 0)
149
150     print "  stale:", dict.get("stale", False)
151
152     print "  elapsed_since_update:", int(dict.get("elapsed_since_update", 0))
153
154     server_states = dict.get("server_states", [])
155     if server_states:
156         print "  server states:", ",".join(server_states)
157
158     if glo_client_kind == "digdugclient":
159         print "  server version:", dict.get("server_version", 0)
160         client_states = dict.get("client_states", [])
161         if client_states:
162             print "  client states:", ",".join(client_states)
163
164 def do_inc_version(args):
165     glo_client.inc_version(glo_options.channel)
166
167 def do_set_channel(args):
168     if len(args) != 1:
169        verbose_exit(EXIT_BADARG, "invalid argument to set_channel")
170
171     print "digdug: setting channel to", args[0]
172
173     glo_config.set("DEFAULT", "channel", args[0])
174     save_config_file()
175
176 def do_clear_state(args):
177     if len(args) < 1:
178         verbose_exit(EXIT_BADARG, "syntax: digdug clearstate <state> <state> ...")
179
180     new_states = []
181     for x in args:
182         new_states.append("-" + x)
183     glo_client.set_state(glo_options.channel, new_states)
184
185 def do_set_state(args):
186     if len(args) < 1:
187         verbose_exit(EXIT_BADARG, "syntax: digdug setstate <state> <state> ...")
188
189     new_states = args[:]
190
191     if glo_client_kind == "digdugserver":
192         print "digdug: broadcasting server state '" + "/".join(new_states) + "' to clients"
193     else:
194         print "digdig: setting client state to '" + "/".join(new_states) + "'"
195
196     glo_client.set_state(glo_options.channel, new_states)
197
198 def do_verify_version(args):
199     print "digdug: verifying client/server version match"
200     glo_client.verify_version(glo_options.channel)
201
202 def do_waitfor(args):
203     if len(args) < 1:
204         verbose_exit(EXIT_BADARG, "syntax: digdug waitfor <state>")
205
206     state = args[0]
207
208     if glo_options.count > 1:
209         print "digdug: waiting for at least", glo_options.count, "clients to enter state '" + state + "'"
210     else:
211         print "digdug: waiting for state '" + state + "'"
212
213     ready_list = []
214     badversion_list = []
215
216     while True:
217         result = glo_client.waitfor(glo_options.channel, state, count = glo_options.count, new_version = glo_options.newversion, timeout = 5)
218
219         # print result
220
221         for ready_dict in result.get("list", []):
222             client_hostname = ready_dict["hostname"]
223             if not client_hostname in ready_list:
224                 ready_list.append(client_hostname)
225                 print "  " + state + "(" + str(min(len(ready_list), result.get("count",0))) + "):", client_hostname
226
227         for bv_dict in result.get("badversion_list", []):
228             client_hostname = bv_dict["hostname"]
229             if not client_hostname in badversion_list:
230                 badversion_list.append(client_hostname)
231                 # printing this is likely to confuse people...
232                 # print "  badversion:", client_hostname, bv_dict["client_version"], bv_dict["server_version"]
233
234         # break on success
235         if result.get("result") == "ok":
236             print "digdug: successful completion of waitfor '" + state + "'"
237             break
238
239 def do_subscribe(args):
240     if len(args) < 2:
241         verbose_exit(EXIT_BADARG, "syntax: digdug subscribe hostname:post channel [channel ...]")
242
243     if glo_client_kind != "digdugclient":
244         verbose_exit(EXIT_BADARG, "subscribe can only be used on a digdug client")
245
246     wantChannels = args[1:]
247
248     print "digdug: setting subscription address to", args[0]
249     print "digdug: subscribing to channel(s)", ", ".join(wantChannels)
250
251     glo_client.subscribe(args[0], wantChannels)
252
253 def do_purge(args):
254     glo_client.purge_unsubscribed()
255
256 def do_show(args):
257     channel = glo_options.channel
258     print "digdug: showing channel status for", channel
259     show_channel(channel)
260
261 def do_showchannels(args):
262     print ",".join(glo_client.get_channels())
263
264 def do_showclients(args):
265
266     if glo_client_kind != "digdugserver":
267         verbose_exit(EXIT_BADARG, "subscribe can only be used on a digdug client")
268
269     channel = glo_options.channel
270     print "digdug: showing clients for", channel
271     clients = glo_client.get_clients()
272     l = []
273     for key in clients.keys():
274         client_dict = clients[key]
275
276         addr = client_dict["addr"]
277         hostname = addr[0]
278         port = addr[1]
279
280         # use the hostname if available
281         if "hostname" in client_dict:
282             hostname = client_dict["hostname"]
283
284         if "channels" in client_dict:
285             channel_dict = client_dict["channels"].get(channel, None)
286         else:
287             channel_dict = None
288
289         # do some reformatting
290         dict = {}
291         dict["addr"] = str(hostname) + ":" + str(port)
292         dict["subscribed"] = client_dict.get("subscribed", "False")
293         dict["age"] = int(client_dict.get("age", 0))
294         #dict["timestamp"] = int(client_dict.get("timestamp", 0))
295
296         if channel_dict != None:
297             channel_dict = channel_dict.copy()
298             channel_dict["client_states"] = "/".join(channel_dict.get("client_states", []))
299             #channel_dict["server_states"] = "/".join(channel_dict.get("server_states", []))
300             if "server_version" in channel_dict:
301                 del channel_dict["server_version"]
302             if "server_states" in channel_dict:
303                 del channel_dict["server_states"]
304
305             if "userdata" in channel_dict:
306                 # take things in the userdata part and move them to the main dict
307                 dict.update(channel_dict["userdata"])
308                 del channel_dict["userdata"]
309
310             dict.update(channel_dict)
311
312         l.append(dict)
313
314     fields = ravenlib.tableprint.build_fields(l, keyFields=["addr"])
315
316     ravenlib.tableprint.print_table(l, fields, ravenlib.tableprint.TableFormatterAscii(sys.stdout))
317
318 def do_owl(args):
319     print "[digdugclient]"
320     print "status = running"
321     print "version =", glo_client_version+"-"+glo_client_release
322
323     channels = glo_client.get_channels()
324
325     print "channel =", glo_options.channel
326
327     if glo_options.channel in channels:
328         dict = glo_client.get_channel(glo_options.channel)
329         stale = dict.get("stale", False)
330         client_version = dict.get("version",0)
331         server_version = dict.get("server_version",0)
332         if stale:
333             print "sync =", "stale"
334             print "client_states ="  # although we could report client states here, it confuses the owl display, so don't
335             print "server_states ="
336         elif client_version > server_version:
337             print "sync =", "toonew"
338             print "client_states ="
339             print "server_states ="
340         elif client_version < server_version:
341             print "sync =", "old"
342             print "client_states ="
343             print "server_states ="
344         else:
345             print "sync =", "good"
346             print "client_states =", "/".join(dict.get("client_states", []))
347             print "server_states =", "/".join(dict.get("server_states", []))
348
349     # Below here is stuff to print data on all channels the client knows
350     # about. unsure how useful this is.
351
352     if False:
353         print "all_channels =", ", ".join(channels)
354
355         client_states = []
356         for channel in channels:
357             dict = glo_client.get_channel(channel)
358             client_states.append(channel + ": " + "/".join(dict.get("client_states", [])))
359
360         server_states = []
361         for channel in channels:
362             dict = glo_client.get_channel(channel)
363             server_states.append(channel + ": " + "/".join(dict.get("server_states", [])))
364
365         print "all_client_states =", ", ".join(client_states)
366         print "all_server_states =", ", ".join(server_states)
367
368 def do_setuser(args):
369     if (len(args)<2):
370         verbose_exit(EXIT_BADARG, "syntax: digdug setuser <name> <value>")
371     glo_client.set_user_data(glo_options.channel, args[0], args[1])
372
373 def try_server(x):
374     ravenlib.report.debug("Checking for digdug at " + x)
375     if not os.path.exists(x):
376         return False
377
378     sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
379     try:
380         sock.connect(x)
381     except Exception, e:
382         ravenlib.report.info("Exception " + str(e) + " while trying " + x)
383         return False
384
385     return True
386
387 def connect_digdug():
388     global glo_options
389     global glo_client
390     global glo_client_kind
391     global glo_client_version
392     global glo_client_release
393
394     if glo_options.addr != None:
395         addr = glo_options.addr
396     elif glo_options.server:
397         addr = "uds:/tmp/digdugserver"
398     elif try_server("/tmp/digdugclient"):
399         addr = "uds:/tmp/digdugclient"
400     elif try_server("/tmp/digdugserver"):
401         addr = "uds:/tmp/digdugserver"
402     else:
403         verbose_exit_during_connect(EXIT_NODIGDUG, "not running", "Failed to locate a digdug client or server")
404
405     try:
406         glo_client = DigDugClient(addr)
407         (glo_client_kind, glo_client_version, glo_client_release) = glo_client.get_kind()
408     except socket.error, e:
409         if e.args[0] == 2:
410             verbose_exit_during_connect(EXIT_NODIGDUG, "enoent", "Failed to find digdug socket at " + str(addr))
411         elif e.args[0] ==  111:
412             verbose_exit_during_connect(EXIT_NODIGDUG, "eperm", "Permission error when trying to open digdug socket at " + str(addr))
413         else:
414             raise
415
416 def main():
417     global glo_options
418     global glo_command
419
420     read_config_file()
421
422     parser = create_parser()
423     (glo_options, args) = parser.parse_args()
424
425     if len(args) <= 0:
426         print "No command given. Use -h for help."
427         return -1
428
429     if glo_options.verbose:
430        ravenlib.report.getLogger().setLevel(ravenlib.report.DEBUG)
431
432     glo_command = args[0]
433     command = glo_command
434     (cmd_opts, cmd_args) = create_cmd_parser(command).parse_args(args[1:])
435     for attr in dir(cmd_opts):
436         setattr(glo_options, attr, getattr(cmd_opts, attr))
437
438     if command in ["hello", "clearstate", "incversion", "purge", "setstate", "verifyversion", "waitfor",
439                    "subscribe", "show", "showchannels", "showclients", "showowl", "setuser"]:
440         connect_digdug()
441         ravenlib.report.info("Established connection to " + glo_client_kind + " version " + glo_client_version + "-" + glo_client_release)
442
443     if command == "clearstate":
444        do_clear_state(cmd_args)
445     elif command == "incversion":
446        do_inc_version(cmd_args)
447     elif command == "purge":
448        do_purge(cmd_args)
449     elif command == "setchannel":
450        do_set_channel(cmd_args)
451     elif command == "setstate":
452        do_set_state(cmd_args)
453     elif command == "verifyversion":
454        do_verify_version(cmd_args)
455     elif command == "waitfor":
456        do_waitfor(cmd_args)
457     elif command == "subscribe":
458        do_subscribe(cmd_args)
459     elif command == "show":
460        do_show(cmd_args)
461     elif command == "showchannels":
462        do_showchannels(cmd_args)
463     elif command == "showclients":
464        do_showclients(cmd_args)
465     elif command == "showowl":
466        do_owl(cmd_args)
467     elif command == "setuser":
468        do_setuser(cmd_args)
469
470 def main_exception_wrapper():
471    try:
472        main()
473    except ravenlib.xmlrpc.client.ServerException, e:
474        if (e.name == "BadVersionException"):
475            verbose_exit(EXIT_BADVERSION, e.one_liner())
476        elif (e.name == "NotSubscribedException"):
477            verbose_exit(EXIT_NOTSUBSCRIBED, e.one_liner())
478        elif (e.name == "StaleException"):
479            verbose_exit(EXIT_STALE, e.one_liner())
480        else:
481            raise
482
483 if __name__=="__main__":
484    main_exception_wrapper()