Create helper methods for specifying face creation via topology
and changes to enable users to run NLSR in Mini-NDN-Wifi.

Refs: #5232
Change-Id: Iac8bd170f34985aa2b7ee080033a6ceaf334bd0c
diff --git a/minindn/apps/nlsr.py b/minindn/apps/nlsr.py
index 280a5a4..c6a2c53 100644
--- a/minindn/apps/nlsr.py
+++ b/minindn/apps/nlsr.py
@@ -41,8 +41,16 @@
     SYNC_PSYNC = 'psync'
 
     def __init__(self, node, logLevel='NONE', security=False, sync=SYNC_PSYNC,
-                 faceType='udp', nFaces=3, routingType=ROUTING_LINK_STATE):
+                 faceType='udp', nFaces=3, routingType=ROUTING_LINK_STATE, faceDict=None):
         Application.__init__(self, node)
+        try:
+            from mn_wifi.node import Node_wifi
+            if isinstance(node, Node_wifi) and faceDict == None:
+                warn("Wifi nodes need to have faces configured manually. Please see \
+                      documentation on provided helper methods.\r\n")
+                sys.exit(1)
+        except ImportError:
+            pass
 
         self.network = '/ndn/'
         self.node = node
@@ -63,6 +71,8 @@
         self.sync = sync
         self.faceType = faceType
         self.infocmd = 'infoedit -f nlsr.conf'
+        # Expected format- node : tuple (node name, IP, cost)
+        self.faceDict = faceDict
 
         self.parameters = self.node.params['params']
 
@@ -181,7 +191,10 @@
     def createConfigFile(self):
 
         self.__editGeneralSection()
-        self.__editNeighborsSection()
+        if self.faceDict:
+            self.__editNeighborsSectionManual()
+        else:
+            self.__editNeighborsSection()
         self.__editHyperbolicSection()
         self.__editFibSection()
         self.__editAdvertisingSection()
@@ -225,6 +238,22 @@
                           .format(self.infocmd, self.network, other.name, other.name,
                                   self.faceType, ip, linkCost))
 
+    def __editNeighborsSectionManual(self):
+
+        self.node.cmd('{} -d neighbors.neighbor'.format(self.infocmd))
+        if self.node not in self.faceDict:
+            return
+        for link in self.faceDict[self.node]:
+            nodeName = link[0]
+            nodeIP = link[1]
+            linkCost = link[2]
+
+            self.node.cmd('{} -a neighbors.neighbor \
+                          <<<\'name {}{}-site/%C1.Router/cs/{} face-uri {}://{}\n link-cost {}\''
+                          .format(self.infocmd, self.network, nodeName, nodeName,
+                                  self.faceType, nodeIP, linkCost))
+
+
     def __editHyperbolicSection(self):
 
         self.node.cmd('{} -s hyperbolic.state -v {}'.format(self.infocmd, self.hyperbolicState))
@@ -254,4 +283,4 @@
             self.node.cmd('{} -s security.prefix-update-validator.trust-anchor.file-name -v security/site.cert'.format(self.infocmd))
             self.node.cmd('{} -p security.cert-to-publish -v security/site.cert'.format(self.infocmd))
             self.node.cmd('{} -p security.cert-to-publish -v security/op.cert'.format(self.infocmd))
-            self.node.cmd('{} -p security.cert-to-publish -v security/router.cert'.format(self.infocmd))
+            self.node.cmd('{} -p security.cert-to-publish -v security/router.cert'.format(self.infocmd))
\ No newline at end of file
diff --git a/minindn/minindn.py b/minindn/minindn.py
index 10dbcb4..597e5c6 100644
--- a/minindn/minindn.py
+++ b/minindn/minindn.py
@@ -38,7 +38,6 @@
 from mininet.util import ipStr, ipParse
 from mininet.log import info, debug, error
 
-
 class Minindn(object):
     """
     This class provides the following features to the user:
@@ -83,10 +82,11 @@
         else:
             self.topoFile = topoFile
 
+        self.faces_to_create = {}
         if topo is None and not noTopo:
             try:
                 info('Using topology file {}\n'.format(self.topoFile))
-                self.topo = self.processTopo(self.topoFile)
+                self.topo, self.faces_to_create = self.processTopo(self.topoFile)
             except configparser.NoSectionError as e:
                 info('Error reading config file: {}\n'.format(e))
                 sys.exit(1)
@@ -122,13 +122,15 @@
 
         # nargs='?' required here since optional argument
         parser.add_argument('topoFile', nargs='?', default='/usr/local/etc/mini-ndn/default-topology.conf',
-                            help='If no template_file is given, topologies/default-topology.conf will be used.')
+                            help='If no template_file is given, topologies/default-topology.conf \
+                            will be used.')
 
         parser.add_argument('--work-dir', action='store', dest='workDir', default='/tmp/minindn',
                             help='Specify the working directory; default is /tmp/minindn')
 
         parser.add_argument('--result-dir', action='store', dest='resultDir', default=None,
-                            help='Specify the full path destination folder where experiment results will be moved')
+                            help='Specify the full path destination folder where experiment \
+                            results will be moved')
 
         return parser
 
@@ -201,6 +203,28 @@
 
             topo.addLink(link[0], link[1], **params)
 
+        faces = {}
+        try:
+            items = config.items('faces')
+            debug("Faces")
+            for item in items:
+                face_a, face_b = item[0].split(':')
+                debug(item)
+                cost = -1
+                for param in item[1].split(' '):
+                    if param.split("=")[0] == 'cost':
+                        cost = param.split("=")[1]
+                face_info = (face_b, int(cost))
+                if face_a not in faces:
+                    faces[face_a] = [face_info]
+                else:
+                    faces[face_a].append(face_info)
+        except configparser.NoSectionError:
+            debug("Faces section is optional")
+            pass
+
+        return (topo, faces)
+
         return topo
 
     def start(self):
@@ -249,6 +273,16 @@
         info(format_exc())
         exit(1)
 
+    def getInterfaceDelay(self, node, interface):
+        tc_output = node.cmd("tc qdisc show dev {}".format(interface))
+        for line in tc_output.splitlines():
+            if "qdisc netem 10:" in line:
+                split_line = line.split(" ")
+                for index in range(0, len(split_line)):
+                    if split_line[index] == "delay":
+                        return float(split_line[index + 1][:-2])
+        return 0.0
+
     def initParams(self, nodes):
         """Initialize Mini-NDN parameters for array of nodes"""
         for host in nodes:
@@ -258,4 +292,83 @@
             homeDir = '{}/{}'.format(Minindn.workDir, host.name)
             host.params['params']['homeDir'] = homeDir
             host.cmd('mkdir -p {}'.format(homeDir))
-            host.cmd('export HOME={} && cd ~'.format(homeDir))
\ No newline at end of file
+            host.cmd('export HOME={} && cd ~'.format(homeDir))
+
+    def nfdcBatchProcessing(self, station, faces):
+        # Input format: [IP, protocol, isPermanent]
+        batch_file = open("{}/{}/nfdc.batch".format(Minindn.workDir, station.name), "w")
+        lines = []
+        for face in faces:
+            ip = face[0]
+            protocol = face[1]
+            if face[2]:
+                face_type = "permanent"
+            else:
+                face_type = "persistent"
+            nfdc_command = "face create {}://{} {}\n".format(protocol, ip, face_type)
+            lines.append(nfdc_command)
+        batch_file.writelines(lines)
+        batch_file.close()
+        debug(station.cmd("nfdc -f {}/{}/nfdc.batch".format(Minindn.workDir, station.name)))
+
+    def setupFaces(self, faces_to_create=None):
+        """ Method to create unicast faces between connected nodes;
+            Returns dict- {node: (other node name, other node IP, other node's delay as int)}. 
+            This is intended to pass to the NLSR helper via the faceDict param """
+        if not faces_to_create:
+            faces_to_create = self.faces_to_create
+        # (nodeName, IP, delay as int)
+        # list of tuples
+        created_faces = dict()
+        batch_faces = dict()
+        for nodeAname in faces_to_create.keys():
+            if not nodeAname in batch_faces.keys():
+                batch_faces[nodeAname] = []
+            for nodeBname, faceCost in faces_to_create[nodeAname]:
+                if not nodeBname in batch_faces.keys():
+                    batch_faces[nodeBname] = []
+                nodeA = self.net[nodeAname]
+                nodeB = self.net[nodeBname]
+                if nodeA.connectionsTo(nodeB):
+                    best_interface = None
+                    delay = None
+                    for interface in nodeA.connectionsTo(nodeB):
+                        interface_delay = self.getInterfaceDelay(nodeA, interface[0])
+                        if not delay or int(interface_delay) < delay:
+                            best_interface = interface
+                    faceAIP = best_interface[0].IP()
+                    faceBIP = best_interface[1].IP()
+                    # Node delay will be symmetrical for connected nodes
+                    nodeDelay = int(self.getInterfaceDelay(nodeA, best_interface[0]))
+                    #nodeBDelay = int(self.getInterfaceDelay(nodeB, best_interface[1]))
+                else:
+                    # If no direct wired connections exist (ie when using a switch),
+                    # assume the default interface
+                    faceAIP = nodeA.IP()
+                    faceBIP = nodeB.IP()
+                    nodeADelay = int(self.getInterfaceDelay(nodeA, nodeA.defaultIntf()))
+                    nodeBDelay = int(self.getInterfaceDelay(nodeB, nodeB.defaultIntf()))
+                    nodeDelay = nodeADelay + nodeBDelay
+
+                if not faceCost == -1:
+                    nodeALink = (nodeA.name, faceAIP, faceCost)
+                    nodeBLink = (nodeB.name, faceBIP, faceCost)
+                else:
+                    nodeALink = (nodeA.name, faceAIP, nodeDelay)
+                    nodeBLink = (nodeB.name, faceBIP, nodeDelay)
+
+                # Importing this before initialization causes issues
+                batch_faces[nodeAname].append([faceBIP, "udp", True])
+                batch_faces[nodeBname].append([faceAIP, "udp", True])
+
+                if nodeA not in created_faces:
+                    created_faces[nodeA] = [nodeBLink]
+                else:
+                    created_faces[nodeA].append(nodeBLink)
+                if nodeB not in created_faces:
+                    created_faces[nodeB] = [nodeALink]
+                else:
+                    created_faces[nodeB].append(nodeALink)
+        for station_name in batch_faces.keys():
+            self.nfdcBatchProcessing(self.net[station_name], batch_faces[station_name])
+        return created_faces
\ No newline at end of file
diff --git a/minindn/wifi/minindnwifi.py b/minindn/wifi/minindnwifi.py
index 54b4aad..ba3c434 100644
--- a/minindn/wifi/minindnwifi.py
+++ b/minindn/wifi/minindnwifi.py
@@ -34,17 +34,20 @@
 from mn_wifi.link import WirelessLink
 
 from minindn.minindn import Minindn
+from minindn.helpers.nfdc import Nfdc
 
 class MinindnWifi(Minindn):
     """ Class for handling default args, Mininet-wifi object and home directories """
-    def __init__(self, parser=argparse.ArgumentParser(), topo=None, topoFile=None, noTopo=False, link=WirelessLink, workDir=None, **mininetParams):
-        """Create Mini-NDN-Wifi object
+    def __init__(self, parser=argparse.ArgumentParser(), topo=None, topoFile=None, noTopo=False,
+                 link=WirelessLink, workDir=None, **mininetParams):
+        """
+        Create Mini-NDN-Wifi object
         parser: Parent parser of Mini-NDN-Wifi parser (use to specify experiment arguments)
         topo: Mininet topo object (optional)
         topoFile: topology file location (optional)
         noTopo: Allows specification of topology after network object is initialized (optional)
-        link: Allows specification of default Mininet/Mininet-Wifi link type for connections between nodes (optional)
-        mininetParams: Any params to pass to Mininet-WiFi
+        link: Allows specification of default Mininet/Mininet-Wifi link type for
+        connections between nodes (optional)mininetParams: Any params to pass to Mininet-WiFi
         """
         self.parser = self.parseArgs(parser)
         self.args = self.parser.parse_args()
@@ -63,10 +66,11 @@
         else:
             self.topoFile = topoFile
 
+        self.faces_to_create = {}
         if topo is None and not noTopo:
             try:
                 info('Using topology file {}\n'.format(self.topoFile))
-                self.topo = self.processTopo(self.topoFile)
+                self.topo, self.faces_to_create = self.processTopo(self.topoFile)
             except configparser.NoSectionError as e:
                 info('Error reading config file: {}\n'.format(e))
                 sys.exit(1)
@@ -193,7 +197,27 @@
 
             topo.addLink(link[0], link[1], **params)
 
-        return topo
+        faces = {}
+        try:
+            items = config.items('faces')
+            debug("Faces")
+            for item in items:
+                face_a, face_b = item[0].split(':')
+                debug(item)
+                cost = -1
+                for param in item[1].split(' '):
+                    if param.split("=")[0] == 'cost':
+                        cost = param.split("=")[1]
+                face_info = (face_b, int(cost))
+                if face_a not in faces:
+                    faces[face_a] = [face_info]
+                else:
+                    faces[face_a].append(face_info)
+        except configparser.NoSectionError:
+            debug("Faces section is optional")
+            pass
+
+        return (topo, faces)
 
     def startMobility(self, max_x=1000, max_y=1000, **kwargs):
         """ Method to run a basic mobility setup on your net"""
@@ -203,4 +227,81 @@
     def startMobilityModel(self, max_x=1000, max_y=1000, **kwargs):
         """ Method to run a mobility model on your net until exited"""
         self.net.plotGraph(max_x=max_x, max_y=max_y)
-        self.net.setMobilityModel(**kwargs)
\ No newline at end of file
+        self.net.setMobilityModel(**kwargs)
+
+    def getWifiInterfaceDelay(self, node, interface=None):
+        """Method to return the configured tc delay of a wifi node's interface as a float"""
+        if not interface:
+            wifi_interface = "{}-wlan0".format(node.name)
+        else:
+            wifi_interface = interface
+        tc_output = node.cmd("tc qdisc show dev {}".format(wifi_interface))
+        for line in tc_output.splitlines():
+            if "qdisc netem 10:" in line:
+                split_line = line.split(" ")
+                for index in range(0, len(split_line)):
+                    if split_line[index] == "delay":
+                        return float(split_line[index + 1][:-2])
+        return 0.0
+
+    def setupFaces(self, faces_to_create=None):
+        """
+        Method to create unicast faces between nodes connected by an AP based on name or faces
+        between connected nodes; Returns dict- {node: (other node name, other node IP, other
+        node's delay as int)}. This is intended to pass to the NLSR helper via the faceDict param
+        """
+        if not faces_to_create:
+            faces_to_create = self.faces_to_create
+        # (nodeName, IP, delay as int)
+        # list of tuples
+        created_faces = dict()
+        batch_faces = dict()
+        for nodeAname in faces_to_create.keys():
+            if not nodeAname in batch_faces.keys():
+                batch_faces[nodeAname] = []
+            for nodeBname, faceCost in faces_to_create[nodeAname]:
+                if not nodeBname in batch_faces.keys():
+                    batch_faces[nodeBname] = []
+                nodeA = self.net[nodeAname]
+                nodeB = self.net[nodeBname]
+                if nodeA.connectionsTo(nodeB):
+                    best_interface = None
+                    delay = None
+                    for interface in nodeA.connectionsTo(nodeB):
+                        interface_delay = self.getInterfaceDelay(nodeA, interface[0])
+                        if not delay or int(interface_delay) < delay:
+                            best_interface = interface
+                    faceAIP = best_interface[0].IP()
+                    faceBIP = best_interface[1].IP()
+                    # Node delay should be symmetrical
+                    nodeDelay = int(self.getInterfaceDelay(nodeA, best_interface[0]))
+                else:
+                    # Default IP will be the primary wireless interface, unclear if multiple wireless
+                    # interfaces should be handled
+                    faceAIP = nodeA.IP()
+                    faceBIP = nodeB.IP()
+                    nodeADelay = self.getWifiInterfaceDelay(nodeA)
+                    nodeBDelay = self.getWifiInterfaceDelay(nodeB)
+                    nodeDelay = nodeADelay + nodeBDelay
+
+                if not faceCost == -1:
+                    nodeALink = (nodeA.name, faceAIP, faceCost)
+                    nodeBLink = (nodeB.name, faceBIP, faceCost)
+                else:
+                    nodeALink = (nodeA.name, faceAIP, nodeDelay)
+                    nodeBLink = (nodeB.name, faceBIP, nodeDelay)
+
+                batch_faces[nodeAname].append([faceBIP, "udp", True])
+                batch_faces[nodeBname].append([faceAIP, "udp", True])
+
+                if nodeA not in created_faces:
+                    created_faces[nodeA] = [nodeBLink]
+                else:
+                    created_faces[nodeA].append(nodeBLink)
+                if nodeB not in created_faces:
+                    created_faces[nodeB] = [nodeALink]
+                else:
+                    created_faces[nodeB].append(nodeALink)
+        for station_name in batch_faces.keys():
+            self.nfdcBatchProcessing(self.net[station_name], batch_faces[station_name])
+        return created_faces
\ No newline at end of file