Update bmv2.py to run stratum_bmv2 plus various improvements

Also added alias to quickly run mininet with stratum in cell machines
and p4vm

Change-Id: Id10bf8f3de4fe14d77b5efe47b6129a8a28b5a89
diff --git a/tools/dev/bash_profile b/tools/dev/bash_profile
index 06d3495..8212dbf 100644
--- a/tools/dev/bash_profile
+++ b/tools/dev/bash_profile
@@ -343,4 +343,5 @@
 alias uktopo='onos-netcfg $OCI $ONOS_ROOT/tools/test/topos/uk-cfg.json'
 
 # Mininet command that uses BMv2 instead of OVS
-alias mn-p4='sudo -E mn --custom $BMV2_MN_PY --switch onosbmv2 --controller remote,ip=$OC1'
+alias mn-bmv2='sudo -E mn --custom $BMV2_MN_PY --switch onosbmv2 --controller remote,ip=$OC1'
+alias mn-stratum='sudo -E mn --custom $BMV2_MN_PY --switch stratum --controller remote,ip=$OC1'
diff --git a/tools/dev/mininet/bmv2.py b/tools/dev/mininet/bmv2.py
index 5df07b6..56c7dcc 100644
--- a/tools/dev/mininet/bmv2.py
+++ b/tools/dev/mininet/bmv2.py
@@ -1,3 +1,19 @@
+# coding=utf-8
+"""
+Copyright 2019-present Open Networking Foundation
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+"""
 import multiprocessing
 import os
 
@@ -5,11 +21,12 @@
 import random
 import re
 import socket
+import sys
 import threading
 import time
 import urllib2
 from contextlib import closing
-from mininet.log import info, warn
+from mininet.log import info, warn, debug
 from mininet.node import Switch, Host
 
 SIMPLE_SWITCH_GRPC = 'simple_switch_grpc'
@@ -19,6 +36,17 @@
 BMV2_LOG_LINES = 5
 BMV2_DEFAULT_DEVICE_ID = 1
 
+# Stratum paths relative to stratum repo root
+STRATUM_BMV2 = 'stratum_bmv2'
+STRATUM_BINARY = '/bazel-bin/stratum/hal/bin/bmv2/' + STRATUM_BMV2
+STRATUM_INIT_PIPELINE = '/stratum/hal/bin/bmv2/dummy.json'
+
+
+def getStratumRoot():
+    if 'STRATUM_ROOT' not in os.environ:
+        raise Exception("Env variable STRATUM_ROOT not set")
+    return os.environ['STRATUM_ROOT']
+
 
 def parseBoolean(value):
     if value in ['1', 1, 'true', 'True']:
@@ -41,20 +69,27 @@
 
 
 def watchDog(sw):
-    while True:
-        if ONOSBmv2Switch.mininet_exception == 1:
-            sw.killBmv2(log=False)
-            return
-        if sw.stopped:
-            return
-        with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
-            if s.connect_ex(('127.0.0.1', sw.grpcPort)) == 0:
-                time.sleep(1)
-            else:
-                warn("\n*** WARN: BMv2 instance %s died!\n" % sw.name)
-                sw.printBmv2Log()
-                print ("-" * 80) + "\n"
+    try:
+        writeToFile(sw.keepaliveFile,
+                    "Remove this file to terminate %s" % sw.name)
+        while True:
+            if ONOSBmv2Switch.mininet_exception == 1 \
+                    or not os.path.isfile(sw.keepaliveFile):
+                sw.killBmv2(log=False)
                 return
+            if sw.stopped:
+                return
+            with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
+                if s.connect_ex(('127.0.0.1', sw.grpcPort)) == 0:
+                    time.sleep(1)
+                else:
+                    warn("\n*** WARN: switch %s died ☠️ \n" % sw.name)
+                    sw.printBmv2Log()
+                    print ("-" * 80) + "\n"
+                    return
+    except Exception as e:
+        warn("*** ERROR: " + e.message)
+        sw.killBmv2(log=True)
 
 
 class ONOSHost(Host):
@@ -86,12 +121,13 @@
                  elogger=False, grpcport=None, cpuport=255, notifications=False,
                  thriftport=None, netcfg=True, dryrun=False, pipeconf="",
                  pktdump=False, valgrind=False, gnmi=False,
-                 portcfg=True, onosdevid=None, **kwargs):
+                 portcfg=True, onosdevid=None, stratum=False, **kwargs):
         Switch.__init__(self, name, **kwargs)
         self.grpcPort = grpcport
         self.thriftPort = thriftport
         self.cpuPort = cpuport
         self.json = json
+        self.useStratum = parseBoolean(stratum)
         self.debugger = parseBoolean(debugger)
         self.notifications = parseBoolean(notifications)
         self.loglevel = loglevel
@@ -116,7 +152,11 @@
             self.onosDeviceId = "device:bmv2:%s" % self.name
         self.logfd = None
         self.bmv2popen = None
-        self.stopped = False
+        self.stopped = True
+        # In case of exceptions, mininet removes *.out files from /tmp. We use
+        # this as a signal to terminate the switch instance (if active).
+        self.keepaliveFile = '/tmp/bmv2-%s-watchdog.out' % self.name
+        self.targetName = STRATUM_BMV2 if self.useStratum else SIMPLE_SWITCH_GRPC
 
         # Remove files from previous executions
         self.cleanupTmpFiles()
@@ -226,29 +266,43 @@
             warn("*** WARN: unable to push config to ONOS (%s)\n" % e.reason)
 
     def start(self, controllers):
-        bmv2Args = [SIMPLE_SWITCH_GRPC] + self.grpcTargetArgs()
-        if self.valgrind:
-            bmv2Args = VALGRIND_PREFIX.split() + bmv2Args
 
-        cmdString = " ".join(bmv2Args)
+        if not self.stopped:
+            warn("*** %s is already running!\n" % self.name)
+            return
+
+        # Remove files from previous executions (if we are restarting)
+        self.cleanupTmpFiles()
+
+        if self.grpcPort is None:
+            self.grpcPort = pickUnusedPort()
+        writeToFile("/tmp/bmv2-%s-grpc-port" % self.name, self.grpcPort)
+
+        if self.useStratum:
+            config_dir = "/tmp/bmv2-%s-stratum" % self.name
+            os.mkdir(config_dir)
+            cmdString = self.getStratumCmdString(config_dir)
+        else:
+            if self.thriftPort is None:
+                self.thriftPort = pickUnusedPort()
+            writeToFile("/tmp/bmv2-%s-thrift-port" % self.name, self.thriftPort)
+            cmdString = self.getBmv2CmdString()
 
         if self.dryrun:
-            info("\n*** DRY RUN (not executing bmv2)")
+            info("\n*** DRY RUN (not executing %s)\n" % self.targetName)
 
-        info("\nStarting BMv2 target: %s\n" % cmdString)
-
-        writeToFile("/tmp/bmv2-%s-grpc-port" % self.name, self.grpcPort)
-        writeToFile("/tmp/bmv2-%s-thrift-port" % self.name, self.thriftPort)
+        debug("\n%s\n" % cmdString)
 
         try:
             if not self.dryrun:
                 # Start the switch
+                self.stopped = False
                 self.logfd = open(self.logfile, "w")
                 self.bmv2popen = self.popen(cmdString,
                                             stdout=self.logfd,
                                             stderr=self.logfd)
                 self.waitBmv2Start()
-                # We want to be notified if BMv2 dies...
+                # We want to be notified if BMv2/Stratum dies...
                 threading.Thread(target=watchDog, args=[self]).start()
 
             self.doOnosNetcfg(self.controllerIp(controllers))
@@ -258,11 +312,29 @@
             self.printBmv2Log()
             raise
 
-    def grpcTargetArgs(self):
-        if self.grpcPort is None:
-            self.grpcPort = pickUnusedPort()
-        if self.thriftPort is None:
-            self.thriftPort = pickUnusedPort()
+    def getBmv2CmdString(self):
+        bmv2Args = [SIMPLE_SWITCH_GRPC] + self.bmv2Args()
+        if self.valgrind:
+            bmv2Args = VALGRIND_PREFIX.split() + bmv2Args
+        return " ".join(bmv2Args)
+
+    def getStratumCmdString(self, config_dir):
+        stratumRoot = getStratumRoot()
+        args = [
+            stratumRoot + STRATUM_BINARY,
+            '-device_id=%d' % BMV2_DEFAULT_DEVICE_ID,
+            '-forwarding_pipeline_configs_file=%s/config.txt' % config_dir,
+            '-persistent_config_dir=' + config_dir,
+            '-initial_pipeline=' + stratumRoot + STRATUM_INIT_PIPELINE,
+            '-cpu_port=%s' % self.cpuPort,
+            '-external_hercules_urls=0.0.0.0:%d' % self.grpcPort
+        ]
+        for port, intf in self.intfs.items():
+            if not intf.IP():
+                args.append('%d@%s' % (port, intf.name))
+        return " ".join(args)
+
+    def bmv2Args(self):
         args = ['--device-id %s' % str(BMV2_DEFAULT_DEVICE_ID)]
         for port, intf in self.intfs.items():
             if not intf.IP():
@@ -299,12 +371,17 @@
         while True:
             result = sock.connect_ex(('127.0.0.1', self.grpcPort))
             if result == 0:
+                # No new line
+                sys.stdout.write("⚡️ %s @ %d" % (self.targetName, self.bmv2popen.pid))
+                sys.stdout.flush()
                 # The port is open. Let's go! (Close socket first)
                 sock.close()
                 break
             # Port is not open yet. If there is time, we wait a bit.
             if endtime > time.time():
-                time.sleep(0.1)
+                sys.stdout.write('.')
+                sys.stdout.flush()
+                time.sleep(0.05)
             else:
                 # Time's up.
                 raise Exception("Switch did not start before timeout")
@@ -331,23 +408,35 @@
         return random.choice(clist).IP()
 
     def killBmv2(self, log=False):
+        self.stopped = True
         if self.bmv2popen is not None:
-            self.bmv2popen.kill()
+            self.bmv2popen.terminate()
+            self.bmv2popen.wait()
+            self.bmv2popen = None
         if self.logfd is not None:
             if log:
                 self.logfd.write("*** PROCESS TERMINATED BY MININET ***\n")
             self.logfd.close()
+            self.logfd = None
 
     def cleanupTmpFiles(self):
-        self.cmd("rm -f /tmp/bmv2-%s-*" % self.name)
+        self.cmd("rm -rf /tmp/bmv2-%s-*" % self.name)
 
     def stop(self, deleteIntfs=True):
         """Terminate switch."""
-        self.stopped = True
         self.killBmv2(log=True)
         Switch.stop(self, deleteIntfs)
 
 
+class ONOSStratumSwitch(ONOSBmv2Switch):
+    def __init__(self, name, **kwargs):
+        kwargs["stratum"] = True
+        super(ONOSStratumSwitch, self).__init__(name, **kwargs)
+
+
 # Exports for bin/mn
-switches = {'onosbmv2': ONOSBmv2Switch}
+switches = {
+    'onosbmv2': ONOSBmv2Switch,
+    'stratum': ONOSStratumSwitch,
+}
 hosts = {'onoshost': ONOSHost}