Allow bmv2-demo mininet topology with arbitrary size

+ made imbalanced striping optional
+ improved generated netcfg
+ various improvements to bmv2.py

Change-Id: Ic297a9d4571bc1987a9cf8fe7bec7c648fb86686
diff --git a/tools/dev/mininet/bmv2.py b/tools/dev/mininet/bmv2.py
index c4e27d5..5a4de21 100644
--- a/tools/dev/mininet/bmv2.py
+++ b/tools/dev/mininet/bmv2.py
@@ -5,7 +5,7 @@
 import urllib2
 
 from mininet.log import info, warn, error
-from mininet.node import Switch
+from mininet.node import Switch, Host
 
 if 'ONOS_ROOT' not in os.environ:
     error("ERROR: environment var $ONOS_ROOT not set")
@@ -14,6 +14,23 @@
 BMV2_TARGET = 'simple_switch_grpc'
 ONOS_ROOT = os.environ["ONOS_ROOT"]
 CPU_PORT = 255
+PKT_BYTES_TO_DUMP = 80
+
+
+class ONOSHost(Host):
+    def __init__(self, name, inNamespace=True, **params):
+        Host.__init__(self, name, inNamespace=inNamespace, **params)
+
+    def config(self, **params):
+        r = super(Host, self).config(**params)
+        for off in ["rx", "tx", "sg"]:
+            cmd = "/sbin/ethtool --offload %s %s off" % (self.defaultIntf(), off)
+            self.cmd(cmd)
+        # disable IPv6
+        self.cmd("sysctl -w net.ipv6.conf.all.disable_ipv6=1")
+        self.cmd("sysctl -w net.ipv6.conf.default.disable_ipv6=1")
+        self.cmd("sysctl -w net.ipv6.conf.lo.disable_ipv6=1")
+        return r
 
 
 class ONOSBmv2Switch(Switch):
@@ -24,7 +41,7 @@
 
     def __init__(self, name, json=None, debugger=False, loglevel="warn", elogger=False,
                  persistent=False, grpcPort=None, thriftPort=None, netcfg=True,
-                 pipeconfId="", **kwargs):
+                 pipeconfId="", pktdump=False, **kwargs):
         Switch.__init__(self, name, **kwargs)
         self.grpcPort = ONOSBmv2Switch.pickUnusedPort() if not grpcPort else grpcPort
         self.thriftPort = ONOSBmv2Switch.pickUnusedPort() if not thriftPort else thriftPort
@@ -38,6 +55,7 @@
         self.loglevel = loglevel
         self.logfile = '/tmp/bmv2-%d.log' % self.deviceId
         self.elogger = elogger
+        self.pktdump = pktdump
         self.persistent = persistent
         self.netcfg = netcfg
         self.netcfgfile = '/tmp/bmv2-%d-netcfg.json' % self.deviceId
@@ -102,19 +120,19 @@
             basicCfg["latitude"] = self.latitude
 
         cfgData = {
-                "generalprovider": {
-                    "p4runtime": {
-                        "ip": srcIP,
-                        "port": self.grpcPort,
-                        "deviceId": self.deviceId,
-                        "deviceKeyId": "p4runtime:%s" % self.onosDeviceId
-                    }
-                },
-                "piPipeconf": {
-                    "piPipeconfId": self.pipeconfId
-                },
-                "basic": basicCfg,
-                "ports": portData
+            "generalprovider": {
+                "p4runtime": {
+                    "ip": srcIP,
+                    "port": self.grpcPort,
+                    "deviceId": self.deviceId,
+                    "deviceKeyId": "p4runtime:%s" % self.onosDeviceId
+                }
+            },
+            "piPipeconf": {
+                "piPipeconfId": self.pipeconfId
+            },
+            "basic": basicCfg,
+            "ports": portData
         }
 
         return cfgData
@@ -161,6 +179,8 @@
         if self.debugger:
             args.append('--debugger')
         args.append('--log-console')
+        if self.pktdump:
+            args.append('--pcap --dump-packet-data %d' % PKT_BYTES_TO_DUMP)
         args.append('-L%s' % self.loglevel)
         args.append('--thrift-port %d' % self.thriftPort)
         if not self.json:
@@ -206,3 +226,4 @@
 
 # Exports for bin/mn
 switches = {'onosbmv2': ONOSBmv2Switch}
+hosts = {'onoshost': ONOSHost}
diff --git a/tools/test/topos/bmv2-demo.py b/tools/test/topos/bmv2-demo.py
index 4aa7eb5..171cc6f 100755
--- a/tools/test/topos/bmv2-demo.py
+++ b/tools/test/topos/bmv2-demo.py
@@ -4,6 +4,7 @@
 import sys
 import json
 import argparse
+from collections import OrderedDict
 
 TEMP_NETCFG_FILE = '/tmp/bmv2-demo-cfg.json'
 BASE_LONGITUDE = -115
@@ -27,7 +28,7 @@
     RUN_PACK_PATH = os.environ["RUN_PACK_PATH"]
 
 from onos import ONOSCluster, ONOSCLI
-from bmv2 import ONOSBmv2Switch
+from bmv2 import ONOSBmv2Switch, ONOSHost
 
 from itertools import combinations
 from time import sleep
@@ -44,11 +45,15 @@
 class ClosTopo(Topo):
     "2 stage Clos topology"
 
-    def __init__(self, pipeconfId="", **opts):
+    def __init__(self, args, **opts):
         # Initialize topology and default options
         Topo.__init__(self, **opts)
 
-        bmv2SwitchIds = ["s11", "s12", "s13", "s21", "s22", "s23"]
+        bmv2SwitchIds = []
+        for row in (1, 2):
+            for col in range(1, args.size + 1):
+                bmv2SwitchIds.append("s%d%d" % (row, col))
+
         bmv2Switches = {}
 
         for switchId in bmv2SwitchIds:
@@ -65,21 +70,22 @@
                                                     netcfg=False,
                                                     longitude=longitude,
                                                     latitude=latitude,
-                                                    pipeconfId=pipeconfId)
+                                                    pipeconfId=args.pipeconf_id)
 
-        for i in (1, 2, 3):
-            for j in (1, 2, 3):
+        for i in range(1, args.size + 1):
+            for j in range(1, args.size + 1):
                 if i == j:
                     # 2 links
                     self.addLink(bmv2Switches["s1%d" % i], bmv2Switches["s2%d" % j],
                                  cls=TCLink, bw=DEFAULT_SW_BW)
-                    self.addLink(bmv2Switches["s1%d" % i], bmv2Switches["s2%d" % j],
-                                 cls=TCLink, bw=DEFAULT_SW_BW)
+                    if args.with_imbalanced_striping:
+                        self.addLink(bmv2Switches["s1%d" % i], bmv2Switches["s2%d" % j],
+                                     cls=TCLink, bw=DEFAULT_SW_BW)
                 else:
                     self.addLink(bmv2Switches["s1%d" % i], bmv2Switches["s2%d" % j],
                                  cls=TCLink, bw=DEFAULT_SW_BW)
 
-        for hostId in (1, 2, 3):
+        for hostId in range(1, args.size + 1):
             host = self.addHost("h%d" % hostId,
                                 cls=DemoHost,
                                 ip="10.0.0.%d/24" % hostId,
@@ -87,28 +93,14 @@
             self.addLink(host, bmv2Switches["s1%d" % hostId], cls=TCLink, bw=DEFAULT_HOST_BW)
 
 
-class DemoHost(Host):
+class DemoHost(ONOSHost):
     "Demo host"
 
-    def __init__(self, name, inNamespace=True, **params):
-        Host.__init__(self, name, inNamespace=inNamespace, **params)
+    def __init__(self, name, **params):
+        ONOSHost.__init__(self, name, **params)
         self.exectoken = "/tmp/mn-exec-token-host-%s" % name
         self.cmd("touch %s" % self.exectoken)
 
-    def config(self, **params):
-        r = super(Host, self).config(**params)
-
-        for off in ["rx", "tx", "sg"]:
-            cmd = "/sbin/ethtool --offload %s %s off" % (self.defaultIntf(), off)
-            self.cmd(cmd)
-
-        # disable IPv6
-        self.cmd("sysctl -w net.ipv6.conf.all.disable_ipv6=1")
-        self.cmd("sysctl -w net.ipv6.conf.default.disable_ipv6=1")
-        self.cmd("sysctl -w net.ipv6.conf.lo.disable_ipv6=1")
-
-        return r
-
     def startPingBg(self, h):
         self.cmd(self.getInfiniteCmdBg("ping -i0.5 %s" % h.IP()))
         self.cmd(self.getInfiniteCmdBg("arping -w5000000 %s" % h.IP()))
@@ -146,7 +138,10 @@
 
 
 def generateNetcfg(onosIp, net, args):
-    netcfg = {'devices': {}, 'links': {}, 'hosts': {}}
+    netcfg = OrderedDict()
+    netcfg['devices'] = {}
+    netcfg['links'] = {}
+    netcfg['hosts'] = {}
     # Device configs
     for sw in net.switches:
         srcIp = sw.getSourceIp(onosIp)
@@ -177,13 +172,14 @@
             hostLocations[sw2.name] = '%s/%s' % (sw1.onosDeviceId, port1)
             continue
 
-        linkId = '%s/%s-%s/%s' % (sw1.onosDeviceId, port1, sw2.onosDeviceId, port2)
-        netcfg['links'][linkId] = {
-            'basic': {
-                'type': 'DIRECT',
-                'bandwidth': 50
+        for linkId in ('%s/%s-%s/%s' % (sw1.onosDeviceId, port1, sw2.onosDeviceId, port2),
+                       '%s/%s-%s/%s' % (sw2.onosDeviceId, port2, sw1.onosDeviceId, port1)):
+            netcfg['links'][linkId] = {
+                'basic': {
+                    'type': 'DIRECT',
+                    'bandwidth': DEFAULT_SW_BW
+                }
             }
-        }
 
     # Host configs
     longitude = BASE_LONGITUDE
@@ -207,6 +203,14 @@
         }
         netcfg['hosts'][hostId] = hostConfig
 
+    netcfg["apps"] = {
+        "org.onosproject.core": {
+            "core": {
+                "linkDiscoveryMode": "STRICT"
+            }
+        }
+    }
+
     print "Writing network config to %s" % TEMP_NETCFG_FILE
     with open(TEMP_NETCFG_FILE, 'w') as tempFile:
         json.dump(netcfg, tempFile, indent=4)
@@ -220,7 +224,7 @@
         controller = RemoteController('c0', ip=args.onos_ip)
         onosIp = args.onos_ip
 
-    topo = ClosTopo(pipeconfId=args.pipeconf_id)
+    topo = ClosTopo(args)
 
     net = Mininet(topo=topo, build=False, controller=[controller])
 
@@ -265,6 +269,10 @@
         description='BMv2 mininet demo script (2-stage Clos topology)')
     parser.add_argument('--onos-ip', help='ONOS-BMv2 controller IP address',
                         type=str, action="store", required=False)
+    parser.add_argument('--size', help='Number of leaf/spine switches',
+                        type=int, action="store", required=False, default=2)
+    parser.add_argument('--with-imbalanced-striping', help='Topology with imbalanced striping',
+                        type=bool, action="store", required=False, default=False)
     parser.add_argument('--pipeconf-id', help='Pipeconf ID for switches',
                         type=str, action="store", required=False, default='')
     args = parser.parse_args()