import repository from arizona
[raven.git] / owl / server / test.py
1 #!/usr/bin/env python
2 """
3 Author: John H. Hartman <jhh@cs.arizona.edu>
4 Date: 2010-3-17
5
6 This is the mod_python index file for Owl.
7
8 This borrows heavily from the index file written by Justin Samuel for the Raven Demo.
9
10 Owl is organized as follows. The data are stored in a MySQL database. There is one MySQL
11 database per Owl database. The "nodes" table contains the information collected on the 
12 Owl clients. The field names are of the form "module.field". The "modules" table contains
13 information about each module, including its name, heading, alignment, and version.
14 The "columns" table contains information about each column (field), including the name, 
15 heading and alignment.
16
17
18 This script will require:
19 mysql
20 mod_python
21 python-MySQLdb
22 python-json
23 """
24
25 import sys
26 from optparse import OptionParser
27
28
29 version = "0.17"
30 released = "4/22/10"
31 protocol = "1.0"
32
33
34 release = "alpha"
35
36
37 config = {}
38 config["userpasswordfile"] = "/root/mysql-owl-pass.txt"
39 config["userpassword"] = None
40 config["rootpasswordfile"] = "/root/mysql-root-pass.txt"
41 config["rootpassword"] = None
42 config["host"] = "localhost"
43 config["user"] = "owl"
44 config["prefix"] = "owl-"
45 config["url"] = "/owl/"
46 config["path"] = "/usr/local/owl"
47
48 # Parse command line arguments
49
50 oparser = OptionParser(version="%prog " + str(version))
51 (options,args) = oparser.parse_args()
52
53 from mod_python import apache, psp
54 from cgi import escape
55 from urllib import unquote
56 from distutils.version import LooseVersion
57 import json as json_module # avoid name conflict with json function
58 import MySQLdb
59 import ConfigParser
60 import traceback
61 from db import *
62 import time
63
64
65 def owl_dbname(name):
66     global config
67     return config["prefix"] + name
68
69 request = None
70
71 def info(msg):
72     global request
73     sys.stderr.write("info\n")
74     sys.stderr.write(msg)
75     sys.stderr.write("\n")
76     sys.stderr.write(str(request))
77     sys.stderr.write("\n")
78     sys.stderr.flush()
79     if request != None:
80         request.log_error(msg, apache.APLOG_INFO)
81     else:
82         apache.log_error(msg, apache.APLOG_INFO)
83
84 def debug(msg):
85     global request
86     sys.stderr.write("debug\n")
87     sys.stderr.flush()
88     if request != None:
89         request.log_error(msg, apache.APLOG_DEBUG)
90     else:
91         apache.log_error(msg, apache.APLOG_DEBUG)
92
93 info("Owl %s %s %s" % (release, version, released))
94
95 # Now parse the owl config file
96
97 parser = ConfigParser.SafeConfigParser()
98 if release == "production":
99     confFile = "/etc/owl-server.conf"
100 else:
101     confFile = "/etc/owl-server-%s.conf" % (release)
102
103 parser.read(confFile)
104 for item in parser.items("Owl"):
105     (key, value) = item
106     config[key] = value
107
108 # Perform type conversions on the configuration values.
109
110 info("Happy Birthday!")
111 info(time.ctime(time.time()))
112
113 # DB connection info 
114 DB.host = config["host"]
115 DB.user = config["user"]
116 DB.name = None
117 DB.passwd = config["userpassword"]
118 if DB.passwd == None:
119     try:
120         file = config["userpasswordfile"]
121         fd = open(file, "r")
122         DB.passwd = fd.readline().rstrip()
123         fd.close()
124     except:
125         error('Unable to read mysql owl password from "%s"' % (file))
126         error('euid = %s, egid = %s' % (os.geteuid(), os.getegid()))
127         raise
128
129
130
131 #
132 # Display the database contents
133 #
134
135 def index(req, db=None, sortby="basic.host", filterby="basic.host", filterval="*", showheader="true", newer='0', f1='0'):
136     global version, released, config, request
137
138     request = req
139     req.content_type = "text/html"
140     debug("index db=%s sortby=%s newer=%s" % (db,sortby,newer))
141     if db == None:
142         # Show available databases
143         dbconn = DB(name='information_schema')
144         cursor = dbconn.execute("SELECT `SCHEMA_NAME` FROM `SCHEMATA` ORDER BY `SCHEMA_NAME`")
145         rows = cursor.fetchall()
146         dbs = []
147         for row in rows:
148             name = row[0]
149             debug("name = %s" % name)
150             prefix = config["prefix"]
151             if name.startswith(config["prefix"]):
152                 dbs.append(name[len(prefix):])
153         tmpl = psp.PSP(req, filename="templates/index.psp")
154         tmpl.run(vars = { 'dbs' : dbs, 'version' : version, 'date' : released, "config" : config,
155             "release" : release})
156         dbconn.close()
157     else:
158         # Display the specified database.
159
160         # Compute the cut-off date for records. If newer starts with '-' it is relative to the current time.
161
162         if (newer[0] == '-'):
163             now = time.time()
164             suffix = newer[-1]
165             if suffix in string.digits or suffix == 's':
166                 x = int(newer[1:-1])
167                 delta = x
168             elif suffix == 'h':
169                 x = int(newer[1:-1])
170                 delta = x * 3600
171             elif suffix == 'd':
172                 x = int(newer[1:-1])
173                 delta = x * 86400
174             elif suffix == 'w':
175                 x = int(newer[1:-1])
176                 delta = 7 * 86400
177             elif suffix == 'm':
178                 x = int(newer[1:-1]) 
179                 delta = 28 * 86400
180             elif suffix == 'y':
181                 x = int(newer[1:-1]) 
182                 delta =365 * 86400
183             newer = now - delta
184             debug("now = %s, delta = %s, newer = %s" % (now,delta,newer))
185         else:
186             newer = int(newer)
187         dbconn = DB(name=owl_dbname(db))
188         tmpl = psp.PSP(req, filename="templates/display.psp")
189         #tmpl.display_code()
190         tmpl.run(vars = {'dbconn' : dbconn, 
191                 'db' : db, 'sortby' : sortby, 'filterby': filterby,
192                 'filterval': filterval, 'showheader': (showheader=="true"), 'version' : version,
193                 'date': released, 'newer' : newer, 'f1' : f1, "config": config, 
194                 "release" : release})
195         dbconn.close()
196         request = None
197
198 def create_db(req, db):
199     global request
200
201     request = req
202     debug("Creating database %s" % (db))
203     dbname = owl_dbname(db)
204     dbconn = DB(name="information_schema")
205     cursor = dbconn.execute("SELECT * FROM `SCHEMATA` WHERE `SCHEMA_NAME` = '%s'" % (dbname))
206     row = cursor.fetchone()
207     if row == None:
208         try:
209             file = config["rootpasswordfile"]
210             fd = open(file, "r")
211             password = fd.readline().rstrip()
212             fd.close()
213         except:
214             error('Unable to read mysql root password from "%s"' % (file))
215             raise
216         dbconn = DB(user="root", passwd=password, name="information_schema")
217         debug("Creating database")
218         dbconn.execute("CREATE DATABASE IF NOT EXISTS `%s`" % (dbname))
219         dbconn.execute("GRANT ALL PRIVILEGES ON `%s` . * TO 'owl'@'%%'" % (dbname))
220         dbconn.execute("GRANT ALL PRIVILEGES ON `%s` . * TO 'owl'@'localhost'" % (dbname))
221
222         dbconn = DB(name=dbname)
223         debug("Creating node table")
224
225         #
226         dbconn.execute("""CREATE TABLE IF NOT EXISTS `nodes`
227            (`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY) ENGINE = MYISAM ;""")
228         debug("Creating modules table")
229         dbconn.execute("""CREATE TABLE IF NOT EXISTS `modules`
230            (`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
231             `module` VARCHAR(255) NOT NULL,
232             `order` INT NOT NULL,
233             `fields` VARCHAR(1024) NOT NULL,
234             `heading` VARCHAR(255) NOT NULL,
235             `version` VARCHAR(255) NOT NULL) ENGINE = MYISAM ;""")
236         debug("Creating columns table")
237         dbconn.execute("""CREATE TABLE IF NOT EXISTS `columns`
238            (`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
239             `field` VARCHAR(255) NOT NULL,
240             `module` VARCHAR(255) NOT NULL,
241             `heading` VARCHAR(255) NOT NULL,
242             `headingAlign` VARCHAR(255) NOT NULL,
243             `align` VARCHAR(255) NOT NULL,
244             `stats` VARCHAR(255) NOT NULL) ENGINE = MYISAM ;""")
245         result = 1
246     else:
247         result = 0
248     request = None
249     return result
250
251 def register_module(req, db, order, module, version, modHeading, fields, fnames): 
252     dbconn = DB(name=owl_dbname(db))
253     debug("Registering module '%s'" % (module))
254     debug("Checking module version")
255     cursor = dbconn.execute("""SELECT VERSION FROM `modules` WHERE `module` = '%s'""" % (str(module)))
256     row = cursor.fetchone()
257     if row != None:
258         curVersion = str(row[0])
259         if LooseVersion(version) < LooseVersion(curVersion) :
260             debug("ERROR version %s is less than registered version %s" % (version, curVersion))
261             req.write("ERROR version of module %s %s is less than registered version %s" % 
262                     (module, version, curVersion))
263             return 1
264         elif LooseVersion(version) > LooseVersion(curVersion) :
265             # Remove the old version information
266             debug("Deleting old module information")
267             cursor = dbconn.execute("SELECT field FROM `columns` WHERE `module` = '%s'" % (module))
268             for row in cursor.fetchall():
269                 field = row[0]
270                 dbconn.execute("ALTER TABLE `nodes` DROP `%s`" % (field))
271             dbconn.execute("DELETE FROM `columns` WHERE `module` = '%s'" % (module))
272             dbconn.execute("DELETE FROM `modules` WHERE `module` = '%s'" % (module))
273             addit = True
274         else:
275             # Same version, do nothing. Maybe we should verify that nothing has changed?
276             debug("Same version")
277             addit = False
278     else:
279         debug("Module does not exist")
280         addit = True
281     if addit:
282         debug("Adding module")
283         for f in fields:
284             debug('field = "%s"' % (str(f)))
285             (name,module,type,heading,headingAlign,align,stats) = f.split(',')
286             debug("Adding %s to nodes table" % (name))
287             if type == "string":
288                 type = "VARCHAR(255)"
289             elif type == 'integer':
290                 type = 'INT'
291             dbconn.execute("ALTER TABLE `nodes` ADD `%s` %s NOT NULL" % (name, type))
292             dbconn.execute("""INSERT INTO ``columns` (`field`, `module`, `heading`, `headingAlign`,`align`,`stats`) VALUES 
293                 ('%s', '%s', '%s', '%s', '%s', '%s')""" % (str(name), str(module), str(heading), str(headingAlign), str(align),
294                 str(stats)))
295         debug("Inserting module %s to modules table" % (module))
296         dbconn.execute("""INSERT INTO `modules` (`module`, `order`, `version`, `fields`, `heading`) VALUES 
297                 ('%s', '%d', '%s', '%s', '%s')""" % (str(module), order, str(version), str(fnames), str(modHeading)))
298     debug("Done registering module")
299     return 0
300
301 #
302 # Register a module in the databases, creating the databases if necessary. This is invoked
303 # by the Owl client during initialization.
304 #
305
306 def register(req):
307     global protocol, request
308
309     request = req
310     debug("register")
311     debug(str(req))
312     req.content_type = "text/plain"
313     try:
314         req_protocol = req.form.getfirst("protocol")
315         module = req.form.getfirst("module")
316         version = req.form.getfirst("version")
317         order = req.form.getfirst("order", 50)
318         modHeading = req.form.getfirst("heading")
319         databases = req.form.getlist("db")
320         fields = req.form.getlist("field")
321         fnames = req.form.getfirst("fields")
322     except:
323         msg = "error parsing form"
324         debug(msg)
325         req.write("ERROR " + msg)
326         #req.write(traceback.format_exc())
327         request = None
328         return
329     if req_protocol != protocol:
330         msg = "Wrong OWL protocol version: is %s, should be %s" % (req_protocol, protocol)
331         debug(msg)
332         req.write("ERROR " + msg)
333         request = None
334         return
335     for db in databases:
336         debug("registering %s in %s" % (module, db))
337         dbname = owl_dbname(db)
338         created = create_db(req, db)
339         if created:
340             status = register_module(req, db, 0,'_internal', '1.0', 'Data Collection',
341                 ('_internal.timestamp,_internal,integer,Timestamp,center,right,None',
342                  '_internal.date,_internal,string,Date,center,center,None'), 'timestamp,date')
343             if status:
344                 debug("register of %s failed, returning ERROR" % ('_internal'))
345                 request = None
346                 return
347         status = register_module(req, db, order+100, module, version, modHeading, fields, fnames)
348         if status:
349             debug("register of %s failed, returning ERROR" % (module))
350             request = None
351             return
352     debug("register done")
353     req.write("OK")
354     request = None
355
356 #
357 # Update the information in the databases. This is invoked by the Owl client periodically.
358 #
359
360 def update(req):
361     global request
362
363     request = req
364     debug("update")
365     req.content_type = "text/plain"
366     try:
367         databases = req.form.getlist("db")
368         host = req.form.getfirst("basic.host", "")
369     except:
370         msg = "error parsing form"
371         debug(msg)
372         req.write("ERROR " + msg)
373         #req.write(traceback.format_exc())
374         request = None
375         return
376     if req_protocol != protocol:
377         msg = "Wrong OWL protocol version: is %d, should be %d" % (req_protocol, protocol)
378         debug(msg)
379         req.write("ERROR " + msg)
380         request = None
381         return
382     timestamp = time.time()
383     date = time.ctime(timestamp)
384     for db in databases:
385         dbname = owl_dbname(db)
386         dbconn = DB(name=dbname)
387         fields = req.form.keys()
388         while (fields.count("db") > 0):
389             fields.remove("db")
390         valid = []
391         cursor = dbconn.execute("SELECT field FROM `columns`")
392         rows = cursor.fetchall()
393         for row in rows:
394             valid.append(row[0])
395         assignments = []
396         for f in fields:
397             if f not in valid:
398                 debug("ERROR invalid field '%s'" % f)
399                 req.write("ERROR invalid field '%s'" % f)
400                 dbconn.close()
401                 request = None
402                 return
403             assignments.append("`%s` = '%s'" % (f, req.form.getfirst(f,"")))
404         assignments.append("`_internal.timestamp` = '%d'" % (timestamp))
405         assignments.append("`_internal.date` = '%s'" % (date))
406         assignStr = ",".join(assignments)
407         cursor = dbconn.execute("SELECT * FROM `nodes` WHERE `basic.host` = '%s'" % (host))
408         if cursor.rowcount > 0:
409             dbconn.execute("UPDATE `nodes` SET %s WHERE `basic.host` = '%s'" % (assignStr,host))
410         else: 
411             dbconn.execute("INSERT INTO `nodes` SET %s" % (assignStr))
412         dbconn.close()
413     req.write("OK")
414     debug("update done")
415     request = None
416     #return apache.OK
417
418
419 def delete(req, db):
420     global request
421
422     request = req
423     debug("delete %s" % (db))
424     dbname = owl_dbname(db)
425     req.content_type = "text/plain"
426     if db != None:
427         try:
428             file = "/root/mysql-root-pass.txt"
429             fd = open(file, "r")
430             password = fd.readline().rstrip()
431             fd.close()
432         except:
433             error('Unable to read mysql root password from "%s"' % (file))
434             raise
435         dbconn = DB(name="information_schema")
436         cursor = dbconn.execute("SELECT * FROM `SCHEMATA` WHERE `SCHEMA_NAME` = '%s'" % (dbname))
437         row = cursor.fetchone()
438         if row != None:
439             dbconn=DB(name=dbname, user="root", passwd=password)
440             dbconn.execute("DROP DATABASE `%s`" % (dbname))
441         dbconn.close()
442     req.write("OK")
443     request = None
444
445 def xml(req, db=None):
446     global request
447
448     request = req
449     req.content_type = "text/xml"
450     if db != None:
451         dbconn=DB(name=owl_dbname(db))
452         tmpl = psp.PSP(req, filename="templates/xml.psp")
453         tmpl.run(vars = {'db' : db, 'dbconn' : dbconn, "config" : config, "release":release})
454         dbconn.close()
455     request = None
456
457 def json(req, db=None, what="data"):
458     global request
459
460     request = req
461     req.content_type = "text/plain"
462     if db != None:
463         dbconn=DB(name=owl_dbname(db))
464         dbconn.dict_cursor() # switch to the dictionary cursor
465         if (what == "modules" or what == "all"):
466             cursor = dbconn.execute("SELECT * FROM `modules` ORDER BY `order`,`module`")
467             data = cursor.fetchall()
468             req.write(json_module.write(data))
469         if (what == "all"):
470             req.write("\n")     
471         if (what == "data" or what == "all"):
472             cursor = dbconn.execute("SELECT * FROM `nodes` ORDER BY `basic.host`")
473             data = cursor.fetchall()
474             req.write(json_module.write(data))
475         dbconn.cursor() # go back to the normal cursor
476         dbconn.close()
477     request = None
478
479 def versionCompare(x,y):
480     """ sort mapData by version number """
481     try:
482         x = LooseVersion(x[3])
483         test = x.version
484     except AttributeError:
485         x = LooseVersion("0")
486
487     try:
488         y = LooseVersion(y[3])
489         test = y.version
490     except AttributeError:
491         y = LooseVersion("0")
492
493     if (x<y):
494         return -1
495     elif (x==y):
496         return 0
497     else:
498         return 1
499
500 def updateLegend(legend, color, val, index, lowToHigh=True):
501     """ legend = dictionary of legend dicts
502         color = color [0..4]
503         val = value of datapoint
504         index = index of data point; used later for sorting
505         lowToHigh = if true, index goes from low values to high.
506                     if false, index goes from high values to low.
507     """
508
509     if (val == None) or (val == ""):
510         return
511
512     if not color in legend:
513         legend[color] = {'color': color, 'min': None, 'max': None, 'index': index, 'count': 0}
514
515     if lowToHigh:
516         if (legend[color]['min']==None):
517             legend[color]['min'] = val
518         legend[color]['max'] = val
519     else:
520         if (legend[color]['max']==None):
521             legend[color]['max'] = val
522         legend[color]['min'] = val
523
524     legend[color]['count'] = legend[color]['count'] + 1
525
526 def finalizeLegend(legend):
527     """ convert legend from dictionary of dictionaries to a sorted
528         list of lists """
529
530     legend_array = []
531     for key in legend.keys():
532         legend_array.append(legend[key])
533
534     legend_array.sort(lambda x, y: x["index"]-y["index"])
535
536     results = []
537     for item in legend_array:
538         if (item['min'] == item['max']):
539             line = [item['color'], str(item['min']) + " <font size=1>[" + str(item['count']) + "]</font>" ]
540         else:
541             line = [item['color'], str(item['min']) + " - " + str(item['max']) + " <font size=1>[" + str(item['count']) + "]</font>" ]
542         results.append(line)
543     return results
544
545 def makeLegendHtml(legend):
546     """ make a html version of the legend. this should be moved to the java
547         client. only done here because I don't know how to do it in java.
548     """
549     icons = ["http://www.google.com/intl/en_us/mapfiles/marker_grey.png",
550              "http://www.google.com/intl/en_us/mapfiles/ms/micons/blue-dot.png",
551              "http://www.google.com/intl/en_us/mapfiles/ms/micons/red-dot.png",
552              "http://www.google.com/intl/en_us/mapfiles/ms/micons/green-dot.png",
553              "http://www.google.com/intl/en_us/mapfiles/ms/micons/yellow-dot.png"]
554
555     html = ""
556     html = html + '<table>'
557     for item in legend:
558         html = html + '<tr>'
559         html = html + '<td>'
560         html = html + '<img src="' + icons[item[0]] + '"></img>'
561         html = html + ' ' + item[1]
562         html = html + '</td>'
563         html = html + '</tr>'
564     html = html + '</table>'
565
566     return html
567
568 def versionProcess(data):
569     """ a postprocessor to map version numbers to colors. The most recent
570         version is blue. Version (n-1) is colored green, (n-2) yellow, and
571         remaining older versions are colored red.
572
573         NOTE: eventually migrate this out of owl server and into mapviewer
574     """
575     colors = [0,1,3,4,2]  # grey, blue, green, yellow, red
576     legend = {}
577
578     work = []
579     for item in data:
580         work.append([item[0], item[1], item[2], item[3]])
581
582     work.sort(versionCompare)
583     work.reverse() # reverse it so we can work from most recent to least
584
585     lastValue = None
586     colorIndex = 0
587     index = 0
588     for item in work:
589         if item[3] != lastValue:
590             colorIndex += 1
591             if (colorIndex >= len(colors)):
592                 colorIndex = len(colors)-1
593         lastValue = item[3]
594         item[3] = colors[colorIndex]
595         index = index + 1
596
597         updateLegend(legend, colors[colorIndex], lastValue, index, False)
598
599     return (work, finalizeLegend(legend))
600
601 def percentileProcess(data):
602     """ a simple postprocessor for getMapData that separates the data into
603         four percential groups: 0 < 25% < 50% < 75% < 100% and colors them
604         red, yellow, green, and blue, respectively.
605
606         NOTE: eventually migrate this out of owl server and into mapviewer
607     """
608     legend = {}
609     results = []
610     work = []
611     for item in data:
612         if dfloat(item[3], 0) <= 0:
613             results.append([item[0], item[1], item[2], 0])
614         else:
615             work.append([item[0], item[1], item[2], item[3]])
616
617     work.sort(lambda x, y: int(100*(float(x[3])-float(y[3]))))
618
619     index = 0
620     for item in work:
621         if (index < len(work)/4):
622             color = 2 # red
623         elif (index < 2*len(work)/4):
624             color = 4 # yellow
625         elif (index < 3*len(work)/4):
626             color = 3 # green
627         else:
628             color = 1 # blue
629         lastValue = item[3]
630         item[3] = color
631         index = index + 1
632
633         updateLegend(legend, color, lastValue, index, True)
634
635     results.extend(work)
636
637     return (results, finalizeLegend(legend))
638
639 def dfloat(x, default):
640     try:
641          return float(x)
642     except:
643          return default
644
645 def dint(x, default):
646     try:
647          return int(x)
648     except:
649          return default
650
651 def getMapData(req, db=None, postprocess=None, colorfield="mapviewer.color"):
652     req.content_type = "text/plain"
653     if db != None:
654         dbconn = DB(name = owl_dbname(db))
655         cursor = dbconn.execute("SELECT `mapviewer.latitude`, `mapviewer.longitude`, `basic.host`, `" + colorfield + "` FROM nodes ORDER BY `mapviewer.latitude`, `mapviewer.longitude`, `basic.host`")
656         data = cursor.fetchall()
657
658         # filter out data with lat and long missing
659         filtereddata = []
660         for item in data:
661             if (not item[0]) or (not item[1]):
662                 # lat or long is unknown
663                 pass
664             else:
665                 filtereddata.append(item)
666
667         # apply postprocess filter if desired
668         if postprocess == "percentile":
669             (filtereddata, legend) = percentileProcess(filtereddata)
670         elif postprocess == "version":
671             (filtereddata, legend) = versionProcess(filtereddata)
672         else:
673             legend = [[1, "1"], [2, "2"], [3, "3"], [4, "4"]]
674
675         # format the data to how the mapviewer app expects it
676         formatteddata = []
677         for item in filtereddata:
678             formatteddata.append([dfloat(item[0], 0.0), dfloat(item[1], 0.0), item[2], dint(item[3], 0)])
679
680         req.write(json_module.write([formatteddata, legend, makeLegendHtml(legend)]))
681
682 def reset(req, db=None):
683     if db:
684         dbname = owl_dbname(db)
685         dbconn = DB(name=dbname)
686         dbconn.execute("TRUNCATE `nodes`")
687         dbconn.close()
688     req.write("OK");