add sorting tables to the pcu view.
authorStephen Soltesz <soltesz@cs.princeton.edu>
Wed, 3 Dec 2008 01:09:32 +0000 (01:09 +0000)
committerStephen Soltesz <soltesz@cs.princeton.edu>
Wed, 3 Dec 2008 01:09:32 +0000 (01:09 +0000)
add support for ipmi, blackbox, and a ManualPCU class that will send email to
an local admin.
add a --force option to findbadpcu.py, still need a solution for the global
increment.
added siteid2loginbase mapping to plccache for displaying loginbase in web.

findbadpcu.py
monitor/wrapper/plccache.py
pcucontrol/reboot.py
web/MonitorWeb/monitorweb/controllers.py
web/MonitorWeb/monitorweb/static/javascript/sortable_tables.js [new file with mode: 0644]
web/MonitorWeb/monitorweb/templates/pculist.kid
web/MonitorWeb/monitorweb/templates/sitemenu.kid

index 158d772..8ebd891 100755 (executable)
@@ -317,7 +317,7 @@ def checkAndRecordState(l_pcus, cohash):
                fbnodesync.flush()
 
                node_round   = fbnodesync.round
                fbnodesync.flush()
 
                node_round   = fbnodesync.round
-               if node_round < global_round:
+               if node_round < global_round or config.force:
                        # recreate node stats when refreshed
                        #print "%s" % nodename
                        req = threadpool.WorkRequest(collectPingAndSSH, [pcuname, cohash], {}, 
                        # recreate node stats when refreshed
                        #print "%s" % nodename
                        req = threadpool.WorkRequest(collectPingAndSSH, [pcuname, cohash], {}, 
@@ -416,6 +416,7 @@ if __name__ == '__main__':
                                                dbname="findbadpcus", 
                                                cachenodes=False,
                                                refresh=False,
                                                dbname="findbadpcus", 
                                                cachenodes=False,
                                                refresh=False,
+                                               force=False,
                                                )
        parser.add_option("-f", "--nodelist", dest="nodelist", metavar="FILE", 
                                                help="Provide the input file for the node list")
                                                )
        parser.add_option("-f", "--nodelist", dest="nodelist", metavar="FILE", 
                                                help="Provide the input file for the node list")
@@ -434,6 +435,8 @@ if __name__ == '__main__':
                                                help="Refresh the cached values")
        parser.add_option("-i", "--increment", action="store_true", dest="increment", 
                                                help="Increment round number to force refresh or retry")
                                                help="Refresh the cached values")
        parser.add_option("-i", "--increment", action="store_true", dest="increment", 
                                                help="Increment round number to force refresh or retry")
+       parser.add_option("", "--force", action="store_true", dest="force", 
+                                               help="Force probe without incrementing global 'round'.")
        parser = parsermodule.getParser(['defaults'], parser)
        config = parsermodule.parse_args(parser)
        try:
        parser = parsermodule.getParser(['defaults'], parser)
        config = parsermodule.parse_args(parser)
        try:
index d4cfbbc..45f879c 100755 (executable)
@@ -74,6 +74,7 @@ l_nodenetworks = None
 plcdb_hn2lb = None
 plcdb_lb2hn = None
 plcdb_netid2ip = None
 plcdb_hn2lb = None
 plcdb_lb2hn = None
 plcdb_netid2ip = None
+plcdb_id2lb = None
 
 def init():
        global l_sites
 
 def init():
        global l_sites
@@ -83,6 +84,7 @@ def init():
        global plcdb_hn2lb
        global plcdb_lb2hn
        global plcdb_netid2ip
        global plcdb_hn2lb
        global plcdb_lb2hn
        global plcdb_netid2ip
+       global plcdb_id2lb
 
        api = plc.getCachedAuthAPI()
        l_sites = api.GetSites({'peer_id':None}, 
 
        api = plc.getCachedAuthAPI()
        l_sites = api.GetSites({'peer_id':None}, 
@@ -101,6 +103,7 @@ def init():
        plcdb_hn2lb = hn2lb
        plcdb_lb2hn = lb2hn
        plcdb_netid2ip = netid2ip
        plcdb_hn2lb = hn2lb
        plcdb_lb2hn = lb2hn
        plcdb_netid2ip = netid2ip
+       plcdb_id2lb = id2lb
        
        return l_nodes
 
        
        return l_nodes
 
index 5e1d6d7..2e6d2d5 100755 (executable)
@@ -117,7 +117,7 @@ class Transport:
        HTTP   = 3
        IPAL   = 4
 
        HTTP   = 3
        IPAL   = 4
 
-       TELNET_TIMEOUT = 60
+       TELNET_TIMEOUT = 120
 
        def __init__(self, type, verbose):
                self.type = type
 
        def __init__(self, type, verbose):
                self.type = type
@@ -204,6 +204,7 @@ class Transport:
                if self.transport != None:
                        output = self.transport.read_until(expected, self.TELNET_TIMEOUT)
                        if output.find(expected) == -1:
                if self.transport != None:
                        output = self.transport.read_until(expected, self.TELNET_TIMEOUT)
                        if output.find(expected) == -1:
+                               print "OUTPUT: --%s--" % output
                                raise ErrorClass, "'%s' not found" % expected
                        else:
                                self.transport.write(buffer + "\r\n")
                                raise ErrorClass, "'%s' not found" % expected
                        else:
                                self.transport.write(buffer + "\r\n")
@@ -278,7 +279,30 @@ class PCUControl(Transport,PCUModel,PCURecord):
                        import traceback
                        traceback.print_exc()
                        return "EOF connection reset" + str(err)
                        import traceback
                        traceback.print_exc()
                        return "EOF connection reset" + str(err)
-               
+
+class IPMI(PCUControl):
+
+       supported_ports = [80,443,623]
+
+       # TODO: get exit codes to determine success or failure...
+       def run(self, node_port, dryrun):
+
+               if not dryrun:
+                       cmd = "ipmitool -I lanplus -H %s -U %s -P '%s' power cycle"
+                       p = os.popen(cmd % ( self.host, self.username, self.password) )
+                       result = p.read()
+                       print "RESULT: ", result
+               else:
+                       cmd = "ipmitool -I lanplus -H %s -U %s -P '%s' user list"
+                       p = os.popen(cmd % ( self.host, self.username, self.password) )
+                       result = p.read()
+                       print "RESULT: ", result
+
+               if "Error" in result:
+                       return result
+               else:
+                       return 0
+                       
 class IPAL(PCUControl):
        """ 
                This now uses a proprietary format for communicating with the PCU.  I
 class IPAL(PCUControl):
        """ 
                This now uses a proprietary format for communicating with the PCU.  I
@@ -1022,6 +1046,15 @@ class ePowerSwitch(PCUControl):
                self.close()
                return 0
                
                self.close()
                return 0
                
+class ManualPCU(PCUControl):
+       supported_ports = [22,23,80,443,9100,16992]
+
+       def run(self, node_port, dryrun):
+               if not dryrun:
+                       # TODO: send email message to monitor admin requesting manual
+                       # intervention.  This should always be an option for ridiculous,
+                       # custom jobs.
+               return 0
 
 ### rebooting european BlackBox PSE boxes
 # Thierry Parmentelat - May 11 2005
 
 ### rebooting european BlackBox PSE boxes
 # Thierry Parmentelat - May 11 2005
@@ -1032,6 +1065,29 @@ class ePowerSwitch(PCUControl):
 # curl --http1.0 --basic --user <username>:<password> --data P<port>=r \
 #      http://<hostname>:<http_port>/cmd.html && echo OK
 
 # curl --http1.0 --basic --user <username>:<password> --data P<port>=r \
 #      http://<hostname>:<http_port>/cmd.html && echo OK
 
+# log in:
+
+## BB PSMaverick
+class BlackBoxPSMaverick(PCUControl):
+       supported_ports = [80]
+
+       def run(self, node_port, dryrun):
+               if not dryrun:
+                       # send reboot signal.
+                       cmd = "curl -s --data 'P%s=r' --anyauth --user '%s:%s' http://%s/config/home_f.html" % ( node_port, self.username, self.password, self.host)
+               else:
+                       # else, just try to log in
+                       cmd = "curl -s --anyauth --user '%s:%s' http://%s/config/home_f.html" % ( self.username, self.password, self.host)
+
+               p = os.popen(cmd)
+               result = p.read()
+               print "RESULT: ", result
+
+               if len(result.split()) > 3:
+                       return 0
+               else:
+                       return result
+
 def bbpse_reboot (pcu_ip,username,password,port_in_pcu,http_port, dryrun):
 
        global verbose
 def bbpse_reboot (pcu_ip,username,password,port_in_pcu,http_port, dryrun):
 
        global verbose
@@ -1275,6 +1331,10 @@ def model_to_object(modelname):
                return WTIIPS4
        elif "ePowerSwitch" in modelname:
                return ePowerSwitch
                return WTIIPS4
        elif "ePowerSwitch" in modelname:
                return ePowerSwitch
+       elif "ipmi" in modelname:
+               return IPMI
+       elif "bbsemaverick" in modelname:
+               return BlackBoxPSMaverick
        else:
                return Unknown
 
        else:
                return Unknown
 
@@ -1303,11 +1363,11 @@ def reboot_test(nodename, values, continue_probe, verbose, dryrun):
                                apc = APCBrazil(values, verbose, ['22', '23'])
                                rb_ret = apc.reboot(values[nodename], dryrun)
 
                                apc = APCBrazil(values, verbose, ['22', '23'])
                                rb_ret = apc.reboot(values[nodename], dryrun)
 
-                       elif values['pcu_id'] in [1221,1225,1220]:
+                       elif values['pcu_id'] in [1221,1225,1220,1192]:
                                apc = APCBerlin(values, verbose, ['22', '23'])
                                rb_ret = apc.reboot(values[nodename], dryrun)
 
                                apc = APCBerlin(values, verbose, ['22', '23'])
                                rb_ret = apc.reboot(values[nodename], dryrun)
 
-                       elif values['pcu_id'] in [1173,1240,47]:
+                       elif values['pcu_id'] in [1173,1240,47,1363,1405,1401,1372,1371]:
                                apc = APCFolsom(values, verbose, ['22', '23'])
                                rb_ret = apc.reboot(values[nodename], dryrun)
 
                                apc = APCFolsom(values, verbose, ['22', '23'])
                                rb_ret = apc.reboot(values[nodename], dryrun)
 
@@ -1375,7 +1435,17 @@ def reboot_test(nodename, values, continue_probe, verbose, dryrun):
                                amt = IntelAMT(values, verbose, ['16992'])
                                rb_ret = amt.reboot(values[nodename], dryrun)
 
                                amt = IntelAMT(values, verbose, ['16992'])
                                rb_ret = amt.reboot(values[nodename], dryrun)
 
-               # BlackBox PSExxx-xx (e.g. PSE505-FR)
+               elif continue_probe and values['model'].find("bbsemaverick") >=0:
+                       print "TRYING BlackBoxPSMaverick"
+                       bbe = BlackBoxPSMaverick(values, verbose, ['80'])
+                       rb_ret = bbe.reboot(values[nodename], dryrun)
+
+               elif continue_probe and values['model'].find("ipmi") >=0:
+
+                       print "TRYING IPMI"
+                       ipmi = IPMI(values, verbose, ['80', '443', '623'])
+                       rb_ret = ipmi.reboot(values[nodename], dryrun)
+
                elif continue_probe and values['model'].find("ePowerSwitch") >=0:
                        # TODO: allow a different port than http 80.
                        if values['pcu_id'] in [1089, 1071, 1046, 1035, 1118]:
                elif continue_probe and values['model'].find("ePowerSwitch") >=0:
                        # TODO: allow a different port than http 80.
                        if values['pcu_id'] in [1089, 1071, 1046, 1035, 1118]:
index d4553dc..2d7f23e 100644 (file)
@@ -5,13 +5,18 @@ from turbogears import controllers, expose, flash
 # log = logging.getLogger("monitorweb.controllers")
 from monitor.database.info.model import *
 from pcucontrol import reboot
 # log = logging.getLogger("monitorweb.controllers")
 from monitor.database.info.model import *
 from pcucontrol import reboot
+from monitor.wrapper.plccache import plcdb_id2lb as site_id2lb
 
 def format_ports(pcu):
        retval = []
        if pcu.port_status and len(pcu.port_status.keys()) > 0 :
                obj = reboot.model_to_object(pcu.plc_pcu_stats['model'])
                for port in obj.supported_ports:
 
 def format_ports(pcu):
        retval = []
        if pcu.port_status and len(pcu.port_status.keys()) > 0 :
                obj = reboot.model_to_object(pcu.plc_pcu_stats['model'])
                for port in obj.supported_ports:
-                       state = pcu.port_status[str(port)]
+                       try:
+                               state = pcu.port_status[str(port)]
+                       except:
+                               state = "unknown"
+                               
                        retval.append( (port, state) )
 
        if retval == []: 
                        retval.append( (port, state) )
 
        if retval == []: 
@@ -106,6 +111,11 @@ class Root(controllers.RootController):
                        else:
                                filtercount['pending'] += 1
                                
                        else:
                                filtercount['pending'] += 1
                                
+                       try:
+                               node.loginbase = site_id2lb[node.plc_pcu_stats['site_id']]
+                       except:
+                               node.loginbase = "unknown"
+
                        node.ports = format_ports(node)
                        node.status = format_pcu_shortstatus(node)
 
                        node.ports = format_ports(node)
                        node.status = format_pcu_shortstatus(node)
 
diff --git a/web/MonitorWeb/monitorweb/static/javascript/sortable_tables.js b/web/MonitorWeb/monitorweb/static/javascript/sortable_tables.js
new file mode 100644 (file)
index 0000000..6ffbe62
--- /dev/null
@@ -0,0 +1,214 @@
+/*
+
+On page load, the SortableManager:
+
+- Finds the table by its id (sortable_table).
+- Parses its thead for columns with a "mochi:format" attribute.
+- Parses the data out of the tbody based upon information given in the
+  "mochi:format" attribute, and clones the tr elements for later re-use.
+- Clones the column header th elements for use as a template when drawing 
+  sort arrow columns.
+- Stores away a reference to the tbody, as it will be replaced on each sort.
+- Performs the first sort.
+
+
+On sort request:
+
+- Sorts the data based on the given key and direction
+- Creates a new tbody from the rows in the new ordering
+- Replaces the column header th elements with clickable versions, adding an
+   indicator (&uarr; or &darr;) to the most recently sorted column.
+
+*/
+
+SortableManager = function () {
+    this.thead = null;
+    this.tbody = null;
+    this.columns = [];
+    this.rows = [];
+    this.sortState = {};
+    this.sortkey = 0;
+};
+
+mouseOverFunc = function () {
+    addElementClass(this, "over");
+};
+
+mouseOutFunc = function () {
+    removeElementClass(this, "over");
+};
+
+ignoreEvent = function (ev) {
+    if (ev && ev.preventDefault) {
+        ev.preventDefault();
+        ev.stopPropagation();
+    } else if (typeof(event) != 'undefined') {
+        event.cancelBubble = false;
+        event.returnValue = false;
+    }
+};
+
+
+update(SortableManager.prototype, {
+
+    "initWithTable": function (table) {
+        /***
+
+            Initialize the SortableManager with a table object
+        
+        ***/
+        // Ensure that it's a DOM element
+        table = getElement(table);
+        // Find the thead
+        this.thead = table.getElementsByTagName('thead')[0];
+        // get the mochi:format key and contents for each column header
+        var cols = this.thead.getElementsByTagName('th');
+        for (var i = 0; i < cols.length; i++) {
+            var node = cols[i];
+            var attr = null;
+            try {
+                attr = node.getAttribute("mochi:format");
+            } catch (err) {
+                // pass
+            }
+            var o = node.childNodes;
+            this.columns.push({
+                "format": attr,
+                "element": node,
+                "proto": node.cloneNode(true)
+            });
+        }
+        // scrape the tbody for data
+        this.tbody = table.getElementsByTagName('tbody')[0];
+        // every row
+        var rows = this.tbody.getElementsByTagName('tr');
+        for (var i = 0; i < rows.length; i++) {
+            // every cell
+            var row = rows[i];
+            var cols = row.getElementsByTagName('td');
+            var rowData = [];
+            for (var j = 0; j < cols.length; j++) {
+                // scrape the text and build the appropriate object out of it
+                var cell = cols[j];
+                var obj = scrapeText(cell);
+                switch (this.columns[j].format) {
+                    case 'isodate':
+                        obj = isoDate(obj);
+                        break;
+                    case 'int':
+                        obj = parseInt(obj);
+                        break;
+                    case 'str':
+                        break;
+                    case 'istr':
+                        obj = obj.toLowerCase();
+                        break;
+                    // cases for numbers, etc. could be here
+                    default:
+                        break;
+                }
+                rowData.push(obj);
+            }
+            // stow away a reference to the TR and save it
+            rowData.row = row.cloneNode(true);
+            this.rows.push(rowData);
+
+        }
+
+        // do initial sort on first column
+        this.drawSortedRows(this.sortkey, true, false);
+
+    },
+
+    "onSortClick": function (name) {
+        /***
+
+            Return a sort function for click events
+
+        ***/
+        return method(this, function () {
+            log('onSortClick', name);
+            var order = this.sortState[name];
+            if (order == null) {
+                order = true;
+            } else if (name == this.sortkey) {
+                order = !order;
+            }
+            this.drawSortedRows(name, order, true);
+        });
+    },
+
+    "drawSortedRows": function (key, forward, clicked) {
+        /***
+
+            Draw the new sorted table body, and modify the column headers
+            if appropriate
+
+        ***/
+        log('drawSortedRows', key, forward);
+        this.sortkey = key;
+        // sort based on the state given (forward or reverse)
+        var cmp = (forward ? keyComparator : reverseKeyComparator);
+        this.rows.sort(cmp(key));
+        // save it so we can flip next time
+        this.sortState[key] = forward;
+        // get every "row" element from this.rows and make a new tbody
+        var newBody = TBODY(null, map(itemgetter("row"), this.rows));
+        // swap in the new tbody
+        this.tbody = swapDOM(this.tbody, newBody);
+               // replace first column with row count.
+               var rows = this.tbody.getElementsByTagName('tr');
+        for (var i=0; i < rows.length; i++) {
+                       var row = rows[i];
+            var cols = row.getElementsByTagName('td');
+                       // var cell = cols[0];
+                       cols[0].innerHTML = String(i);
+               }
+        for (var i = 0; i < this.columns.length; i++) {
+            var col = this.columns[i];
+            var node = col.proto.cloneNode(true);
+            // remove the existing events to minimize IE leaks
+            col.element.onclick = null;
+            col.element.onmousedown = null;
+            col.element.onmouseover = null;
+            col.element.onmouseout = null;
+            // set new events for the new node
+            node.onclick = this.onSortClick(i);
+            node.onmousedown = ignoreEvent;
+            node.onmouseover = mouseOverFunc;
+            node.onmouseout = mouseOutFunc;
+            // if this is the sorted column
+            if (key == i) {
+                // \u2193 is down arrow, \u2191 is up arrow
+                // forward sorts mean the rows get bigger going down
+                var arrow = (forward ? "\u2193" : "\u2191");
+                // add the character to the column header
+                node.appendChild(SPAN(null, arrow));
+                if (clicked) {
+                    node.onmouseover();
+                }
+            }
+            // swap in the new th
+            col.element = swapDOM(col.element, node);
+        }
+    }
+});
+
+sortableManager = new SortableManager();
+
+addLoadEvent(function () {
+    sortableManager.initWithTable('sortable_table');
+});
+
+// rewrite the view-source links
+addLoadEvent(function () {
+    var elems = getElementsByTagAndClassName("A", "view-source");
+    var page = "sortable_tables/";
+    for (var i = 0; i < elems.length; i++) {
+        var elem = elems[i];
+        var href = elem.href.split(/\//).pop();
+        elem.target = "_blank";
+        elem.href = "../view-source/view-source.html#" + page + href;
+    }
+});
index 1ca6e36..d37be12 100644 (file)
@@ -5,14 +5,15 @@ from pcucontrol.reboot import pcu_name, model_to_object
 from monitor import config
 
 def plc_site_link(pcu):
 from monitor import config
 
 def plc_site_link(pcu):
-       return "https://" + config.MONITOR_HOSTNAME + "/db/sites/index.php?id=" + str(pcu['site_id'])
+       return "https://" + config.PLC_WEB_HOSTNAME + "/db/sites/index.php?id=" + str(pcu['site_id'])
 
 def pcu_link(pcu):
 
 def pcu_link(pcu):
-       return "https://" + config.MONITOR_HOSTNAME + "/db/sites/pcu.php?id=" + str(pcu['pcu_id'])
+       return "https://" + config.PLC_WEB_HOSTNAME + "/db/sites/pcu.php?id=" + str(pcu['pcu_id'])
 
 ?>
 <html py:layout="'sitemenu.kid'"
 
 ?>
 <html py:layout="'sitemenu.kid'"
-      xmlns:py="http://purl.org/kid/ns#">
+      xmlns:py="http://purl.org/kid/ns#"
+         xmlns:mochi="http://www.mochi.org">
 
   <div py:match="item.tag == 'content'">
        <table id="sub-table" width="100%">
 
   <div py:match="item.tag == 'content'">
        <table id="sub-table" width="100%">
@@ -28,10 +29,11 @@ def pcu_link(pcu):
                <tbody>
                <tr>
                <td colspan="5">
                <tbody>
                <tr>
                <td colspan="5">
-               <table border="1" width="100%">
+               <table id="sortable_table" class="datagrid" border="1" width="100%">
                        <thead>
                                <tr>
                        <thead>
                                <tr>
-                                       <th>Site</th>
+                                       <th mochi:format="int"></th>
+                                       <th mochi:format="str">Site</th>
                                        <th>PCU Name</th>
                                        <th>Missing Fields</th>
                                        <th>DNS Status</th>
                                        <th>PCU Name</th>
                                        <th>Missing Fields</th>
                                        <th>DNS Status</th>
@@ -43,7 +45,8 @@ def pcu_link(pcu):
                        </thead>
                        <tbody>
                                <tr py:for="i,node in enumerate(query)" class="${i%2 and 'odd' or 'even'}" >
                        </thead>
                        <tbody>
                                <tr py:for="i,node in enumerate(query)" class="${i%2 and 'odd' or 'even'}" >
-                                       <td><a href="${plc_site_link(node.plc_pcu_stats)}">sitename</a></td>
+                                       <td></td>
+                                       <td><a href="${plc_site_link(node.plc_pcu_stats)}">${node.loginbase}</a></td>
                                        <td nowrap="true" >
                                                <a href="${pcu_link(node.plc_pcu_stats)}">${pcu_name(node.plc_pcu_stats)}</a></td>
                                        <td py:content="node.entry_complete"></td>
                                        <td nowrap="true" >
                                                <a href="${pcu_link(node.plc_pcu_stats)}">${pcu_name(node.plc_pcu_stats)}</a></td>
                                        <td py:content="node.entry_complete"></td>
index d46e500..3d15356 100644 (file)
@@ -2,7 +2,10 @@
 <html xmlns:py="http://purl.org/kid/ns#">
   <head>
     <title>App Name - ${page_title}</title>
 <html xmlns:py="http://purl.org/kid/ns#">
   <head>
     <title>App Name - ${page_title}</title>
-    <link href="/static/css/style.css" type="text/css" rel="stylesheet" />
+    <link href="static/css/style.css" type="text/css" rel="stylesheet" />
+    <script type="text/javascript" src="${tg.tg_js}/MochiKit.js"></script>
+    <script type="text/javascript" src="static/javascript/sortable_tables.js"></script>
+
   </head>
 
   <body>
   </head>
 
   <body>