Merge pull request #25 from pingping-lin/ONOS-Next

Add SDN-IP test && update Quagga and ONOS-cli drivers
diff --git a/TestON/dependencies/Jenkins_getresult_andrew.py b/TestON/dependencies/Jenkins_getresult_andrew.py
new file mode 100755
index 0000000..0e7ef8d
--- /dev/null
+++ b/TestON/dependencies/Jenkins_getresult_andrew.py
@@ -0,0 +1,64 @@
+#!/usr/bin/env python
+
+import sys
+import os
+import re
+import datetime
+import time
+import argparse
+
+parser = argparse.ArgumentParser()
+parser.add_argument("-n", "--name", help="Comma Separated string of test names. Ex: --name='test1, test2, test3'")
+args = parser.parse_args()
+
+#Pass in test names as a comma separated string argument. 
+#Example: ./Jenkins_getresult.py "Test1,Test2,Test3,Test4"
+name_list = args.name.split(",")
+result_list = map(lambda x: x.strip(), name_list)
+
+#NOTE: testnames list should be in order in which it is run
+testnames = result_list
+output = ''
+testdate = datetime.datetime.now()
+
+output +="<p>**************************************</p>"
+output +=testdate.strftime('Jenkins test result for %H:%M on %b %d, %Y. %Z')
+
+#TestON reporting
+for test in testnames:
+    name = os.popen("ls /home/admin/ONLabTest/TestON/logs/ -rt | grep %s | tail -1" % test).read().split()[0]
+    path = "/home/admin/ONLabTest/TestON/logs/" + name + "/"
+    output +="<p></p>"
+    #output +="   Date: %s, %s %s" % (name.split("_")[2], name.split("_")[1], name.split("_")[3]) + "<br>*******************<br>"
+    #Open the latest log folder 
+    output += "<h2>Test "+str(test)+"</h2><p>************************************</p>"
+
+    f = open(path + name + ".rpt")
+
+    #Parse through each line of logs and look for specific strings to output to wiki.
+    #NOTE: with current implementation, you must specify which output to output to wiki by using
+    #main.log.report("") since it is looking for the [REPORT] tag in the logs
+    for line in f:
+        if re.search("Result summary for Testcase", line):
+            output += "<h3>"+str(line)+"</h3>"
+            #output += "<br>"
+        if re.search("\[REPORT\]", line): 
+            line_split = line.split("] ")
+            #line string is split by bracket, and first two items (log tags) in list are omitted from output
+            #join is used to convert list to string
+            line_str = ''.join(line_split[2:])
+            output += "<p>"
+            output += line_str
+            output += "</p>"
+        if re.search("Result:", line):
+            output += "<p>"
+            output += line
+            output += "</p>"
+    f.close()
+
+    #*********************
+    #include any other phrase specific to case you would like to include in wiki here
+    if test == "IntentPerf":
+        output += "URL to Historical Performance results data: <a href='http://10.128.5.54/perf.html'>Perf Graph</a>"
+    #*********************
+print output
diff --git a/TestON/drivers/common/cli/onosclidriver.py b/TestON/drivers/common/cli/onosclidriver.py
index 282200d..1ca0a76 100644
--- a/TestON/drivers/common/cli/onosclidriver.py
+++ b/TestON/drivers/common/cli/onosclidriver.py
@@ -981,8 +981,8 @@
             main.cleanup()
             main.exit()
 
-    def add_point_intent(self, ingress_device, port_ingress,
-            egress_device, port_egress, ethType="", ethSrc="",
+    def add_point_intent(self, ingress_device, egress_device,
+            port_ingress="", port_egress="", ethType="", ethSrc="",
             ethDst="", bandwidth="", lambda_alloc=False, 
             ipProto="", ipSrc="", ipDst="", tcpSrc="", tcpDst=""):
         '''
@@ -1017,12 +1017,11 @@
                     and not bandwidth and not lambda_alloc \
                     and not ipProto and not ipSrc and not ipDst \
                     and not tcpSrc and not tcpDst:
-                cmd = "add-point-intent "+\
-                        str(ingress_device) + "/" + str(port_ingress) + " " +\
-                        str(egress_device) + "/" + str(port_egress)
-       
+                cmd = "add-point-intent"
+      
+
             else:
-                cmd = "add-point-intent "
+                cmd = "add-point-intent"
                 
                 if ethType:
                     cmd += " --ethType " + str(ethType)
@@ -1045,18 +1044,39 @@
                 if tcpDst:
                     cmd += " --tcpDst " + str(tcpDst)
 
-                cmd += " "+str(ingress_device) +\
-                    "/" + str(port_ingress) + " " +\
-                    str(egress_device) + "/" + str(port_egress) 
+            #Check whether the user appended the port 
+            #or provided it as an input
+            if "/" in ingress_device:
+                cmd += " "+str(ingress_device) 
+            else:
+                if not port_ingress:
+                    main.log.error("You must specify "+
+                        "the ingress port")
+                    #TODO: perhaps more meaningful return
+                    return main.FALSE
+
+                cmd += " "+ \
+                    str(ingress_device) + "/" +\
+                    str(port_ingress) + " " 
+
+            if "/" in egress_device:
+                cmd += " "+str(egress_device)
+            else:
+                if not port_egress:
+                    main.log.error("You must specify "+
+                        "the egress port")
+                    return main.FALSE
+                
+                cmd += " "+\
+                    str(egress_device) + "/" +\
+                    str(port_egress)  
 
             self.handle.sendline(cmd)
+            
+            main.log.info(cmd + " sent")
             i = self.handle.expect([
                 "Error",
                 "onos>"])
-          
-            self.handle.sendline("intents")
-            self.handle.expect("onos>")
-            Intenthandle = self.handle.before
 
             if i == 0:
                 main.log.error("Error in adding point-to-point intent")
diff --git a/TestON/drivers/common/cli/onosdriver.py b/TestON/drivers/common/cli/onosdriver.py
index 5cc7eb6..ca9fa01 100644
--- a/TestON/drivers/common/cli/onosdriver.py
+++ b/TestON/drivers/common/cli/onosdriver.py
@@ -405,6 +405,11 @@
                 print line
             if report:
                 for line in lines[2:-1]:
+                    #Bracket replacement is for Wiki-compliant
+                    #formatting. '<' or '>' are interpreted 
+                    #as xml specific tags that cause errors
+                    line = line.replace("<","[")
+                    line = line.replace(">","]")
                     main.log.report(line)
             return lines[2]
         except pexpect.EOF:
diff --git a/TestON/tests/IntentPerfNext/IntentPerfNext.py b/TestON/tests/IntentPerfNext/IntentPerfNext.py
index 2fb22dc..bc11ba8 100644
--- a/TestON/tests/IntentPerfNext/IntentPerfNext.py
+++ b/TestON/tests/IntentPerfNext/IntentPerfNext.py
@@ -31,7 +31,8 @@
 
         main.step("Creating cell file")
         cell_file_result = main.ONOSbench.create_cell_file(
-                BENCH_ip, cell_name, MN1_ip, "onos-core",
+                BENCH_ip, cell_name, MN1_ip,
+                "onos-core,onos-app-metrics,onos-gui",
                 ONOS1_ip, ONOS2_ip, ONOS3_ip)
 
         main.step("Applying cell file to environment")
@@ -53,6 +54,9 @@
             build_result = main.TRUE
             main.log.info("Git pull skipped by configuration")
 
+        main.log.report("Commit information - ")
+        main.ONOSbench.get_version(report=True)
+
         main.step("Creating ONOS package")
         package_result = main.ONOSbench.onos_package()
 
@@ -73,11 +77,6 @@
         cli2 = main.ONOS2cli.start_onos_cli(ONOS2_ip)
         cli3 = main.ONOS3cli.start_onos_cli(ONOS3_ip)
 
-        main.step("Enable metrics feature")
-        main.ONOS1cli.feature_install("onos-app-metrics")
-        main.ONOS2cli.feature_install("onos-app-metrics")
-        main.ONOS3cli.feature_install("onos-app-metrics")
-
         utilities.assert_equals(expect=main.TRUE,
                 actual = cell_file_result and cell_apply_result and\
                          verify_cell_result and checkout_result and\
@@ -125,12 +124,19 @@
 
         intent_add_lat_list = []
 
+        #Assign 'linear' switch format for basic intent testing
+        main.Mininet1.assign_sw_controller(
+                sw="1", ip=ONOS1_ip,port1=default_sw_port)
+        main.Mininet1.assign_sw_controller(
+                sw="3", ip=ONOS2_ip,port1=default_sw_port)
+        main.Mininet1.assign_sw_controller(
+                sw="5", ip=ONOS3_ip,port1=default_sw_port)
+
         for i in range(0, int(num_iter)):
-            #add_point_intent(ingr_device, ingr_port, 
-            #                 egr_device, egr_port)
+            #add_point_intent(ingr_device,  egr_device,
+            #                 ingr_port,    egr_port)
             main.ONOS1cli.add_point_intent(
-                device_id_list[0], 2,
-                device_id_list[2], 1)
+                device_id_list[0]+"/2", device_id_list[2]+"/1")
         
             #Allow some time for intents to propagate
             time.sleep(5)
@@ -213,6 +219,104 @@
 
     def CASE3(self, main):
         '''
-        CASE3 coming soon
+        Intent Reroute latency
         '''
+        import time
+        import json
+        import requests
+        import os
+
+        ONOS1_ip = main.params['CTRL']['ip1']
+        ONOS2_ip = main.params['CTRL']['ip2']
+        ONOS3_ip = main.params['CTRL']['ip3']
+        ONOS_user = main.params['CTRL']['user']
+
+        default_sw_port = main.params['CTRL']['port1']
+
+        #number of iterations of case
+        num_iter = main.params['TEST']['numIter']
+
+        #Timestamp keys for json metrics output
+        submit_time = main.params['JSON']['submittedTime']
+        install_time = main.params['JSON']['installedTime']
+        wdRequest_time = main.params['JSON']['wdRequestTime']
+        withdrawn_time = main.params['JSON']['withdrawnTime']
+        devices_json_str = main.ONOS1cli.devices()
+        devices_json_obj = json.loads(devices_json_str)
+
+        device_id_list = []
+
+        #Obtain device id list in ONOS format.
+        #They should already be in order (1,2,3,10,11,12,13, etc)
+        for device in devices_json_obj:
+            device_id_list.append(device['id'])
+
+        #Completes the re-route path by assigning 
+        #additional switches to the topology assigned in case1
+        main.Mininet1.assign_sw_controller(
+                sw="s2",ip1=ONOS2_ip,port1=default_sw_port)
+        main.Mininet1.assign_sw_controller(
+                sw="s4",ip1=ONOS2_ip,port1=default_sw_port)
+
+        intent_reroute_lat_list = []
+
+        for i in range(0, int(num_iter)):
+            #add_point_intent(ingr_device, ingr_port, 
+            #                 egr_device, egr_port)
+            main.ONOS1cli.add_point_intent(
+                device_id_list[0]+"/2", device_id_list[2]+"/1")
+       
+            #TODO: check for correct intent installation
+            time.sleep(5)
+
+            #NOTE: this interface is specific to
+            #      topo-intentFlower.py topology
+            #      reroute case.
+            main.Mininet1.handle.sendline(
+                    "sh ifconfig s2-eth3 down")
+            t0_system = time.time()*1000
+                    
+            #TODO: Check for correct intent reroute
+            time.sleep(5)
+
+            #Obtain metrics from ONOS 1, 2, 3
+            intents_json_str_1 = main.ONOS1cli.intents_events_metrics()
+            intents_json_str_2 = main.ONOS2cli.intents_events_metrics()
+            intents_json_str_3 = main.ONOS3cli.intents_events_metrics()
+
+            intents_json_obj_1 = json.loads(intents_json_str_1)
+            intents_json_obj_2 = json.loads(intents_json_str_2)
+            intents_json_obj_3 = json.loads(intents_json_str_3)
+
+            #Parse values from the json object
+            intent_install_1 = \
+                    intents_json_obj_1[install_time]['value']
+            intent_install_2 = \
+                    intents_json_obj_2[install_time]['value']
+            intent_install_3 = \
+                    intents_json_obj_3[install_time]['value']
+
+            intent_reroute_lat_1 = \
+                    int(intent_install_1) - int(t0_system)
+            intent_reroute_lat_2 = \
+                    int(intent_install_2) - int(t0_system)
+            intent_reroute_lat_3 = \
+                    int(intent_install_3) - int(t0_system)
+            
+            intent_reroute_lat_avg = \
+                    (intent_reroute_lat_1 + 
+                     intent_reroute_lat_2 +
+                     intent_reroute_lat_3 ) / 3
+    
+            main.log.info("Intent reroute latency avg for iteration "+
+                    str(i)+": "+str(intent_reroute_lat_avg))
+
+            if intent_reroute_lat_avg > 0.0 and \
+               intent_reroute_lat_avg < 1000:
+                intent_reroute_lat_list.append(intent_reroute_lat_avg)
+            else:
+                main.log.info("Intent reroute latency exceeded "+
+                        "threshold. Skipping iteration "+str(i))
+
+
 
diff --git a/TestON/tests/MultiProd13/MultiProd13.params b/TestON/tests/MultiProd13/MultiProd13.params
index 2f99555..b60f5cf 100755
--- a/TestON/tests/MultiProd13/MultiProd13.params
+++ b/TestON/tests/MultiProd13/MultiProd13.params
@@ -1,6 +1,6 @@
 <PARAMS>
     
-    <testcases>1,4,5,6,7,8,9</testcases>
+    <testcases>1,4,10,31</testcases>    
 
     #Environment variables
     <ENV>
diff --git a/TestON/tests/MultiProd13/MultiProd13.py b/TestON/tests/MultiProd13/MultiProd13.py
index 1c989c0..d825c62 100755
--- a/TestON/tests/MultiProd13/MultiProd13.py
+++ b/TestON/tests/MultiProd13/MultiProd13.py
@@ -223,9 +223,9 @@
         devices1 = main.ONOScli1.devices()
         devices2 = main.ONOScli2.devices()
         devices3 = main.ONOScli3.devices()
-        print "devices1 = ", devices1
-        print "devices2 = ", devices2
-        print "devices3 = ", devices3
+        #print "devices1 = ", devices1
+        #print "devices2 = ", devices2
+        #print "devices3 = ", devices3
         hosts1 = main.ONOScli1.hosts()
         hosts2 = main.ONOScli2.hosts()
         hosts3 = main.ONOScli3.hosts()
@@ -355,14 +355,33 @@
                 onpass="Topology Check Test successful",
                 onfail="Topology Check Test NOT successful")
 
+
+
+
+    def CASE10(self):
+        main.log.report("This testcase uninstalls the reactive forwarding app")
+        main.log.report("__________________________________")
+        main.case("Uninstalling reactive forwarding app")
+        #Unistall onos-app-fwd app to disable reactive forwarding
+        appUninstall_result1 = main.ONOScli1.feature_uninstall("onos-app-fwd")
+        appUninstall_result2 = main.ONOScli2.feature_uninstall("onos-app-fwd")
+        appUninstall_result3 = main.ONOScli3.feature_uninstall("onos-app-fwd")
+        main.log.info("onos-app-fwd uninstalled")
+
+        #After reactive forwarding is disabled, the reactive flows on switches timeout in 10-15s
+        #So sleep for 15s
+        time.sleep(15)
+
+        case10_result = appUninstall_result1 and appUninstall_result2 and appUninstall_result3
+        utilities.assert_equals(expect=main.TRUE, actual=case10_result,onpass="Reactive forwarding app uninstallation successful",onfail="Reactive forwarding app uninstallation failed")
+
+
     def CASE6(self):
         main.log.report("This testcase is testing the addition of host intents and then doing pingall")
         main.log.report("__________________________________")        
-        main.case("Uninstalling reactive forwarding app and addhost intents")
+        main.case("Obtaining hostsfor adding host intents")
         main.step("Get hosts")
-        main.ONOScli1.handle.sendline("hosts")
-        main.ONOScli1.handle.expect("onos>")
-        hosts = main.ONOScli1.handle.before
+        hosts = main.ONOScli1.hosts()
         main.log.info(hosts)
 
         main.step("Get all devices id")
@@ -371,7 +390,8 @@
 
         #ONOS displays the hosts in hex format unlike mininet which does in decimal format
         #So take care while adding intents
-
+        
+        '''
         main.step("Add host intents for mn hosts(h8-h18,h9-h19,h10-h20,h11-h21,h12-h22,h13-h23,h14-h24,h15-h25,h16-h26,h17-h27)")
         hth_intent_result = main.ONOScli1.add_host_intent("00:00:00:00:00:08/-1", "00:00:00:00:00:12/-1")
         hth_intent_result = main.ONOScli1.add_host_intent("00:00:00:00:00:09/-1", "00:00:00:00:00:13/-1")
@@ -383,16 +403,17 @@
         hth_intent_result = main.ONOScli1.add_host_intent("00:00:00:00:00:0F/-1", "00:00:00:00:00:19/-1")
         hth_intent_result = main.ONOScli1.add_host_intent("00:00:00:00:00:10/-1", "00:00:00:00:00:1A/-1")
         hth_intent_result = main.ONOScli1.add_host_intent("00:00:00:00:00:11/-1", "00:00:00:00:00:1B/-1") 
+        '''
 
-
-        #Unistall onos-app-fwd app to disable reactive forwarding
-        main.step("Unistall onos-app-fwd app to disable reactive forwarding")
-        appUninstall_result = main.ONOScli1.feature_uninstall("onos-app-fwd")
-        main.log.info("onos-app-fwd uninstalled")
-
-        #After reactive forwarding is disabled, the reactive flows on switches timeout in 10-15s
-        #So sleep for 15s
-        time.sleep(15)
+        for i in range(8,18):
+            main.log.info("Adding host intent between h"+str(i)+" and h"+str(i+10))
+            host1 =  "00:00:00:00:00:" + str(hex(i)[2:]).zfill(2).upper()
+            host2 =  "00:00:00:00:00:" + str(hex(i+10)[2:]).zfill(2).upper()
+            #NOTE: get host can return None
+            #TODO: handle this
+            host1_id = main.ONOScli1.get_host(host1)['id']
+            host2_id = main.ONOScli1.get_host(host2)['id']
+            tmp_result = main.ONOScli1.add_host_intent(host1_id, host2_id )
 
         flowHandle = main.ONOScli1.flows()
         #print "flowHandle = ", flowHandle
@@ -634,7 +655,7 @@
 
     def CASE8(self):
         '''
-        Host intents removal
+        Intent removal
         ''' 
         main.log.report("This testcase removes host intents before adding the point intents")
         main.log.report("__________________________________")        
@@ -643,31 +664,37 @@
         main.step("Obtain the intent id's")
         intent_result = main.ONOScli1.intents()
         #print "intent_result = ",intent_result
+        
         intent_linewise = intent_result.split("\n")
-        intent_linewise = intent_linewise[1:-1] #ignore the first and last item of the list obtained from split 
-        #for line in intent_linewise:
-            #print "line = ", line
-        intentids = []
+        intentList = []
         for line in intent_linewise:
+            if line.startswith("id="):
+                intentList.append(line)
+
+        intentids = []
+        for line in intentList:
             intentids.append(line.split(",")[0].split("=")[1])
         for id in intentids:
             print "id = ", id
-        
+
         main.step("Iterate through the intentids list and remove each intent")
         for id in intentids:
             main.ONOScli1.remove_intent(intent_id = id)
-        
+
         intent_result = main.ONOScli1.intents()
         intent_linewise = intent_result.split("\n")
-        intent_linewise = intent_linewise[1:-1] #ignore the first and last item of the list obtained from split 
-        
+        list_afterRemoval = []
+        for line in intent_linewise:
+            if line.startswith("id="):
+                list_afterRemoval.append(line)
+
         intentState = {}
-        for id, line in zip(intentids, intent_linewise):
+        for id, line in zip(intentids, list_afterRemoval):
             #print "line after removing intent = ", line
             x = line.split(",")
             state = x[1].split("=")[1]
             intentState[id] = state
-            
+
         case8_result = main.TRUE
         for key,value in intentState.iteritems():
             print "key,value = ", key, value
@@ -676,12 +703,36 @@
             else:    
                 case8_result = case8_result and main.FALSE
 
-        if case8_result == main.TRUE:
+        i = 8
+        Ping_Result = main.TRUE
+        while i <18 :
+            main.log.info("\n\nh"+str(i)+" is Pinging h" + str(i+10))
+            ping = main.Mininet1.pingHost(src="h"+str(i),target="h"+str(i+10))
+            if ping==main.TRUE:
+                Ping_Result = main.TRUE
+            elif ping==main.FALSE:
+                i+=1
+                Ping_Result = main.FALSE
+            else:
+                main.log.info("Unknown error")
+                Ping_Result = main.ERROR
+        
+        #Note: If the ping result failed, that means the intents have been withdrawn correctly.
+        if Ping_Result==main.TRUE:
+            main.log.report("Host intents have not been withdrawn correctly")
+            #main.cleanup()
+            #main.exit()
+        if Ping_Result==main.FALSE:
+            main.log.report("Host intents have been withdrawn correctly")
+
+        case8_result = case8_result and Ping_Result
+
+        if case8_result == main.FALSE:
             main.log.report("Intent removal successful")
         else:
             main.log.report("Intent removal failed")
                         
-        utilities.assert_equals(expect=main.TRUE, actual=case8_result,
+        utilities.assert_equals(expect=main.FALSE, actual=case8_result,
                 onpass="Intent removal test successful",
                 onfail="Intent removal test failed")
              
@@ -867,3 +918,34 @@
                 onpass="Ping all test after Point intents addition successful",
                 onfail="Ping all test after Point intents addition failed")
 
+    def CASE31(self):
+        ''' 
+            This test case adds point intent related to SDN-IP matching on ICMP (ethertype=IPV4, ipProto=1)
+        '''
+        import json
+
+        main.log.report("This test case adds point intent related to SDN-IP matching on ICMP")
+        main.step("Adding bidirectional point intent")
+        #add-point-intent --ipSrc=10.0.0.8/32 --ipDst=10.0.0.18/32 --ethType=IPV4 --ipProto=1  of:0000000000003008/1 of:0000000000006018/1
+        
+        hosts_json = json.loads(main.ONOScli1.hosts())
+        for  i in range(8,11):
+            main.log.info("Adding point intent between h"+str(i)+" and h"+str(i+10))
+            host1 =  "00:00:00:00:00:" + str(hex(i)[2:]).zfill(2).upper()
+            host2 =  "00:00:00:00:00:" + str(hex(i+10)[2:]).zfill(2).upper()
+            host1_id = main.ONOScli1.get_host(host1)['id']
+            host2_id = main.ONOScli1.get_host(host2)['id']
+            for host in hosts_json:
+                if host['id'] == host1_id:
+                    ip1 = host['ips']+"/"+"32"
+                    device1 = str(host['location']['device'])
+                    port1 = 1
+                    print "ip1 = ", ip1
+                    print "device1 = ", device1
+                elif host['id'] == host2_id:
+                    ip2 = str(host['ips'])+"/"+"32"
+                    device2 = str(host['location']["device"])
+                    port2 = 1
+                    print "ip2 = ", ip2
+                    print "device2 = ", device2
+           
diff --git a/TestON/tests/TopoPerfNext/TopoPerfNext.py b/TestON/tests/TopoPerfNext/TopoPerfNext.py
index a710f82..21f5f44 100644
--- a/TestON/tests/TopoPerfNext/TopoPerfNext.py
+++ b/TestON/tests/TopoPerfNext/TopoPerfNext.py
@@ -75,6 +75,10 @@
             pull_result = main.TRUE
             main.log.info("Skipped git checkout and pull")
 
+        #TODO: Uncomment when wiki posting works
+        #main.log.report("Commit information - ")
+        #main.ONOSbench.get_version(report=True)
+
         main.step("Using mvn clean & install")
         #mvn_result = main.ONOSbench.clean_install()
         mvn_result = main.TRUE
@@ -360,6 +364,17 @@
                     and avg_delta_ofp_graph < threshold_max\
                     and int(i) > iter_ignore:
                 latency_ofp_to_graph_list.append(avg_delta_ofp_graph)
+            elif avg_delta_ofp_graph > (-10) and \
+                    avg_delta_ofp_graph < 0.0 and\
+                    int(i) > iter_ignore:
+                main.log.info("Sub-millisecond result likely; "+
+                    "negative result was rounded to 0")
+                #NOTE: Current metrics framework does not 
+                #support sub-millisecond accuracy. Therefore,
+                #if the result is negative, we can reasonably
+                #conclude sub-millisecond results and just 
+                #append the best rounded effort - 0 ms. 
+                latency_ofp_to_graph_list.append(0)
             else:
                 main.log.info("Results for ofp-to-graph "+\
                         "ignored due to excess in threshold")
@@ -509,7 +524,9 @@
                 "Avg: "+str(latency_end_to_end_avg)+" ms "+
                 "Std Deviation: "+latency_end_to_end_std_dev+" ms")
         main.log.report("Switch add - OFP-to-Graph latency: "+\
-                "Avg: "+str(latency_ofp_to_graph_avg)+" ms "+
+                "Note: results are not accurate to sub-millisecond. "+
+                "Any sub-millisecond results are rounded to 0 ms. ")
+        main.log.report("Avg: "+str(latency_ofp_to_graph_avg)+" ms "+
                 "Std Deviation: "+latency_ofp_to_graph_std_dev+" ms")
         main.log.report("Switch add - TCP-to-OFP latency: "+\
                 "Avg: "+str(latency_tcp_to_ofp_avg)+" ms "+