add netconfig aspect to rpm
[raven.git] / tools / statuscheck / statuscheck.py
1 #! /usr/bin/env python
2
3 """
4 <Program Name>
5    statuscheck.py
6
7 <Started>
8    November, 2007
9
10 <Author>
11    Scott Baker
12
13 <Purpose>
14    Status Check tool for planetlab nodes.
15 """
16
17 #           [option, long option,    variable,     action,        data,     default,                            metavar, description]
18 """arizonaconfig
19    options=[
20             ["",     "--continuous", "continuous", "store_true",  None,     False,                              None,    "continuous mode"],
21             ["",     "--wait",       "wait",       "store_true",  None,     False,                              None,    "wait for signal before starting pass"],
22             ["",     "--daemon",     "daemon",     "store_true",  None,     False,                              None,    "daemonize"],
23             ["",     "--slicename",  "slicenames", "append",      "string", None,                               None,    "name of slice to monitor"],
24             ["",     "--autoadd",    "autoadd",    "append",      "string", None,                               None,    "automatically add nodes for this slice"],
25             ["",     "--options",    "options",    "append",      "string", None,                               None,    "options to pass to remote script"],
26             ["",     "--timeout",    "timeout",    "store",       "int",    600,                                None,    "timeout when running remote script"],
27             ["",     "--concurrent", "concurrent", "store",       "int",    32,                                 None,    "maximum number of concurrent connections"],
28             ["",     "--rate",       "rate",       "store",       "int",    50,                                 None,    "number of connections per minute to attempt"]]
29    includes=[]
30 """
31
32 import os, sys, time, signal, shm, socket
33 import planetlabAPI
34 import planetlabCall
35 import arizonaconfig
36 import arizonaerror
37
38 passes = 1
39 reset = False
40 startup = False
41 continuous = False
42 glo_daemon = False
43
44 resultroot = "results"
45
46
47
48
49
50 def log(msg):
51    """
52    <Purpose>
53       Logs a message. Right now, just print it to the screen; later this may
54       be stored in a log file.
55
56    <Arguments>
57       msg
58          message to log
59
60    <Exceptions>
61       None
62
63    <Returns>
64       None
65    """
66    print msg
67
68    try:
69       logfn = os.path.join(resultroot, "log")
70       logfile = open(logfn, "a")
71       logfile.write(msg)
72       logfile.write("\n")
73       logfile.close()
74    except:
75       pass
76
77
78
79
80
81 def create_pid_file(name):
82    """
83    <Purpose>
84       Create a pid file holding the current process id.
85
86    <Arguments>
87       None
88
89    <Exceptions>
90       None
91
92    <Returns>
93       None
94    """
95    try:
96       file = open(name, "w")
97       file.write(str(os.getpid()))
98       file.close()
99    except:
100       print "error statuscheck_remote create_pid_file"
101
102
103
104
105 def delete_pid_file(name):
106    """
107    <Purpose>
108       Delete the pid file.
109
110    <Arguments>
111       None
112
113    <Exceptions>
114       None
115
116    <Returns>
117       None
118    """
119    try:
120       os.remove(name)
121    except:
122       print "error statuscheck_remote delete_pid_file"
123
124
125
126
127
128
129 def createfile(fn):
130    """
131    <Purpose>
132       Create an empty text file.
133
134    <Arguments>
135       fn
136          filename
137
138    <Exceptions>
139       None
140
141    <Returns>
142       None
143    """
144    file = open(fn, "w")
145    file.close()
146
147
148
149
150
151 def appendfile(fn, what):
152    """
153    <Purpose>
154       Append a line to a text file.
155
156    <Arguments>
157       fn
158          filename
159       what
160          text string to write to file
161
162    <Exceptions>
163       None
164
165    <Returns>
166       None
167    """
168    file = open(fn, "a")
169    file.write(what)
170    file.write("\n")
171    file.close()
172
173
174
175
176
177 def checkconnection(node):
178    """
179    <Purpose>
180       Check a connection by atteming to open a port to the SSH daemon.
181
182    <Arguments>
183       node
184          hostname of node to connect to.
185
186    <Exceptions>
187       None
188
189    <Returns>
190       (True, "Success") if node was connected to
191       (False, error_string) if an error occurred
192    """
193    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
194
195    # set the timeout for 2 minutes
196    s.settimeout(120)
197
198    try:
199       s.connect((node, 22))
200    except Exception, e:
201       error = str(e)
202       return (False, str(e))
203
204    s.close()
205
206    return (True, "Success")
207
208
209
210
211
212 def checknode(slicename, node):
213    """
214    <Purpose>
215       Copy the test script to a node and run it. Store the results in a text
216       file for the node.
217
218    <Arguments>
219       slicename
220          name of slice
221       node
222          hostname of node
223
224    <Side Effects>
225       Disk file for the node is created in results/slicename/hostname
226
227    <Exceptions>
228       None
229
230    <Returns>
231       True if the test script was run
232       False otherwise
233    """
234    resultfn = os.path.join(resultdir, node)
235
236    createfile(resultfn)
237    appendfile(resultfn, "time statuscheck " + str(time.time()))
238
239    (result, err_str) = checkconnection(node)
240    if not result:
241        appendfile(resultfn, "error statuscheck " + err_str.replace(" ","_"))
242        return False
243
244    sliceatnode = slicename + '@' + node
245
246    cmd = 'scp -q -o "StrictHostKeyChecking no" -o "PasswordAuthentication no" statuscheck_remote.py ' + sliceatnode + ':/home/' + slicename
247    print cmd
248    result = os.system(cmd)
249    if result != 0:
250       appendfile(resultfn, "error statuscheck copyfile")
251       return False
252
253    remote_options = arizonaconfig.get_option("options")
254    remote_cmd = "python statuscheck_remote.py"
255    if remote_options:
256       remote_cmd = remote_cmd + " " + " ".join(remote_options)
257
258    cmd = 'ssh -q -o "StrictHostKeyChecking no" -o "PasswordAuthentication no" -n ' + sliceatnode + ' "sudo ' + remote_cmd +'" >> ' + resultfn
259    print cmd
260    result = os.system(cmd)
261    if result != 0:
262       appendfile(resultfn, "error statuscheck runfile")
263       return False
264
265    return True
266
267
268
269
270 glo_semaphone = None
271
272 def checknode_background(slicename, node):
273    """
274    <Purpose>
275       Check the node in the background by forking off a subprocess to handle
276       the check.
277
278    <Arguments>
279       slicename
280          name of slice
281       node
282          name of node
283
284    <Side Effects>
285       Text file for the node is created in results/slicename/node
286
287    <Exceptions>
288       None
289
290    <Returns>
291       None
292    """
293    global glo_concur_semaphore, glo_daemon
294
295    glo_concur_semaphore.P()
296
297    # since the parent won't be waiting on the child, make sure the zombie
298    # goes away
299    signal.signal(signal.SIGCHLD, signal.SIG_IGN)
300
301    # first fork is so that the parent can return and continue running
302    child_pid = os.fork()
303    if child_pid != 0:
304       # we are the parent - return and let the child continue
305       return
306
307    # restore signals, so the child can wait on his own children
308    signal.signal(signal.SIGCHLD, signal.SIG_DFL)
309
310    # next fork is to setup the timeout handler
311    child_pid = os.fork()
312    if child_pid == 0:
313       # we are the child
314       # set our processgroup id to our process id
315       os.setpgid(os.getpid(),0)
316       checknode(slicename, node)
317       sys.exit(0)
318
319    else:
320       # we are the parent - setup a handler to catch the timeout
321       timeout = arizonaconfig.get_option("timeout") # 600
322       tstart = time.time()
323       while (time.time() - tstart) < timeout:
324          ( ret_pid, status ) = os.waitpid(child_pid, os.WNOHANG)
325          if ret_pid == child_pid:
326             glo_concur_semaphore.V()
327             sys.exit(0)
328          time.sleep(1)
329
330       # kill the whole process group belonging to the child
331       os.kill(-child_pid, 15)
332       os.waitpid(child_pid, 0)
333
334       appendfile(os.path.join(resultdir, node), "error statuscheck timeout")
335
336       glo_concur_semaphore.V()
337       sys.exit(-1)
338
339
340
341
342
343 def checknodes(slicename, nodes):
344    """
345    <Purpose>
346       Check all of the nodes by calling checknode_background for each one
347
348    <Arguments>
349       slicename
350          name of the slice
351       nodes
352          list of node hostnames
353
354    <Exceptions>
355       None
356
357    <Returns>
358       None
359    """
360    period = 60.0 / float(arizonaconfig.get_option("rate"))
361    lastTime = time.time()
362    for node in nodes:
363       # figure out how long we should delay
364       timeSleep = period - (time.time()-lastTime)
365       if timeSleep > 0:
366           time.sleep(timeSleep)
367
368       lastTime = time.time()
369       checknode_background(slicename, node);
370
371
372
373
374 def make_daemon():
375    """
376    <Purpose>
377       Turns the process into a daemon. Stolen from arizonageneral
378
379    <Arguments>
380       None
381
382    <Exceptions>
383       None
384
385    <Returns>
386       None
387    """
388    pid = os.fork()
389
390    # TODO: unify with arizonageneral
391
392    # if fork was successful, exit the parent process so it returns
393    try:
394       if pid > 0:
395          os._exit(0)
396    except OSError:
397       print "Error: fork failed, daemon not started"
398       sys.exit(1)
399
400    # close any open files
401    try:
402       sys.stdin.close()
403    except:
404       syslog.syslog("[" + program + "] Error closing stdin")
405    try:
406       sys.stdout.close()
407    except:
408       syslog.syslog("[" + program + "] Error closing stdout")
409    try:
410       sys.stderr.close()
411    except:
412       syslog.syslog("[" + program + "] Error closing stderr")
413    for i in range(1023):
414       try:
415          os.close(i)
416       except OSError:
417          pass
418
419    # redirect stdin/out/err to /dev/null
420    try:
421       sys.stdin = open('/dev/null')       # fd 0
422    except:
423       syslog.syslog("[" + program + "] Error opening new stdin")
424    try:
425       sys.stdout = open('/dev/null', 'w') #open(os.path.join(resultroot,"stdout"), 'w') # fd 1
426    except:
427       syslog.syslog("[" + program + "] Error opening new stdout")
428    try:
429       sys.stderr = open('/dev/null', 'w') #open(os.path.join(resultroot,"stderr"), 'w') # fd 2
430    except:
431       syslog.syslog("[" + program + "] Error opening new stderr")
432
433    # disassociate from parent
434    # os.chdir("/")
435    os.setsid()
436    # os.umask(0)
437
438    return pid
439
440
441
442
443
444 def check_auto_add(slicename, slicenodes, allnodes):
445    """
446    <Purpose>
447       Automatically add available nodes to the slice
448
449    <Arguments>
450       slicename
451          name of slice
452       slicenodes
453          list of hostnames of nodes currently in the slice
454       all nodes
455          list of all available nodes
456
457    <Exceptions>
458       None
459
460    <Returns>
461       None
462    """
463    addnodes = []
464
465    for node in allnodes:
466       hostname = node['hostname']
467       if not (hostname in slicenodes):
468          addnodes.append(hostname)
469          log(slicename + " add " + hostname)
470
471    if addnodes:
472       try:
473          planetlabCall.SliceNodesAdd(slicename, addnodes)
474       except Exception, e:
475          log(slicename + " add " + str(e))
476
477
478
479
480
481 def handler_sighup(signum, frame):
482     """
483     <Purpose>
484        Intercepts the "hangup" signal, but doesn't do anything.
485        Simply causes the sleep to return.
486     """
487     pass
488
489
490
491
492     
493 def main():
494    global continuous, passes
495    global testfile, resultfile
496    global resultdir
497    global glo_daemon
498    global glo_concur_semaphore
499
500    waitforsignal = False
501    glo_daemon = False
502
503    arizonaerror.init_error_reporting("stork.py")
504    arizonaconfig.init_options("statuscheck.py")
505
506    if not os.getenv("SSH_AGENT_PID"):
507       print "must use ssh-agent"
508       sys.exit(1)
509
510    if not arizonaconfig.get_option("slicenames"):
511       print "must use --slicename"
512       sys.exit(1)
513
514    # use a semaphore to limit how many SSH processes can be running at any
515    # one time
516    glo_concur_semaphore = shm.create_semaphore(0, arizonaconfig.get_option("concurrent"))
517
518    slicenames = arizonaconfig.get_option("slicenames")
519    autoadd_slicenames = arizonaconfig.get_option("autoadd")
520    waitforsignal = arizonaconfig.get_option("wait")
521    glo_daemon = arizonaconfig.get_option("daemon")
522
523    if arizonaconfig.get_option("continuous"):
524       passes = 1000000
525
526    planetlabAPI.PlanetLablogin()
527
528    if glo_daemon:
529       print "daemonizing"
530       signal.signal(signal.SIGHUP, handler_sighup)
531       make_daemon()
532
533    create_pid_file(os.path.join(resultroot, "statuscheck.pid"))
534
535    passnum = 0
536    while passnum<passes:
537       passnum = passnum +1
538
539       log("mainloop: pass " + str(passnum))
540
541       # in waitforsignal mode, we wait for someone else (i.e. a cron job) to
542       # create a file called run_now that tells us when to run
543       if waitforsignal:
544           signalfile = os.path.join(resultroot,"run_now")
545           print "waiting for", signalfile
546           while not os.path.exists(signalfile):
547              time.sleep(10)
548           try:
549              os.remove(signalfile)
550           except:
551              pass
552
553       log("mainloop: done waitforsignal")
554
555       if autoadd_slicenames:
556          # if the autoadd feature is being used, then get the list of all nodes
557          try:
558             allnodes = planetlabCall.AdmGetNodes();
559             log("allnodes count " + str(len(allnodes)))
560          except Exception, e:
561             log("allnodes get " + str(e))
562             allnodes = []
563       else:
564          allnodes = []
565
566       log("mainloop: done admgetnodes")
567
568       for slicename in slicenames:
569          log("mainloop: checking slice " + slicename)
570
571          resultdir = os.path.join(resultroot, slicename)
572
573          # make sure resultdir exists
574          try:
575             os.makedirs(resultdir)
576          except OSError:
577             pass
578
579          # get the list of nodes for this slice
580          try:
581             slicenodes = planetlabCall.SliceNodesList(slicename)
582          except Exception, e:
583             log(slicename + " get " + str(e))
584             # skip remainder of processing for this node
585             continue
586
587          log(slicename + " count " + str(len(slicenodes)))
588
589          # if autoadd mode is enabled for this slice, then automatically add
590          # new nodes to it
591          if autoadd_slicenames and (slicename in autoadd_slicenames):
592             check_auto_add(slicename, slicenodes, allnodes)
593
594          log("mainloop: checking nodes")
595
596          # check the nodes for this slice
597          checknodes(slicename, slicenodes)
598
599          log("mainloop: done checking slice " + slicename)
600
601       log("mainloop: done checkslices")
602
603    delete_pid_file(os.path.join(resultdir, "statuscheck.pid"))
604
605    log("main: done")
606
607
608
609
610
611 if __name__ == "__main__":
612    main()