Add wireless simulation to redesigned Mini-NDN using Mininet-wifi.

refs: #4858

Change-Id: If9f7dd069203309c998ab2ff570a6cc8ee362434
diff --git a/.gitignore b/.gitignore
index e56d449..a02d875 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,7 @@
 # Misc
 .DS_Store
 mininet
+mn_wifi
 ndn-src
 openflow
 
diff --git a/examples/wifi/wifi_ping.py b/examples/wifi/wifi_ping.py
new file mode 100644
index 0000000..bd48b15
--- /dev/null
+++ b/examples/wifi/wifi_ping.py
@@ -0,0 +1,83 @@
+# -*- Mode:python; c-file-style:"gnu"; indent-tabs-mode:nil -*- */
+#
+# Copyright (C) 2015-2020, The University of Memphis,
+#                          Arizona Board of Regents,
+#                          Regents of the University of California.
+#
+# This file is part of Mini-NDN.
+# See AUTHORS.md for a complete list of Mini-NDN authors and contributors.
+#
+# Mini-NDN is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Mini-NDN is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Mini-NDN, e.g., in COPYING.md file.
+# If not, see <http://www.gnu.org/licenses/>.
+
+from mininet.log import setLogLevel, info
+from minindn.wifi.minindnwifi import MinindnWifi
+from minindn.util import MiniNDNWifiCLI, getPopen
+from minindn.apps.app_manager import AppManager
+from minindn.apps.nfd import Nfd
+from minindn.helpers.nfdc import Nfdc
+from minindn.helpers.ndnpingclient import NDNPingClient
+from time import sleep
+# This experiment uses the singleap topology and is intended to be a basic
+# test case where we see if two nodes can send interests to each other.
+def runExperiment():
+    setLogLevel('info')
+
+    info("Starting network")
+    ndnwifi = MinindnWifi()
+    a = ndnwifi.net["sta1"]
+    b = ndnwifi.net["sta2"]
+    # Test for model-based mobility
+    if ndnwifi.args.modelMob:
+        ndnwifi.startMobilityModel(model='GaussMarkov')
+    #Test for replay based mobility
+    if ndnwifi.args.mobility:
+        info("Running with mobility...")
+
+        p1, p2, p3, p4 = dict(), dict(), dict(), dict()
+        p1 = {'position': '40.0,30.0,0.0'}
+        p2 = {'position': '40.0,40.0,0.0'}
+        p3 = {'position': '31.0,10.0,0.0'}
+        p4 = {'position': '200.0,200.0,0.0'}
+
+        ndnwifi.net.mobility(a, 'start', time=1, **p1)
+        ndnwifi.net.mobility(b, 'start', time=2, **p2)
+        ndnwifi.net.mobility(a, 'stop', time=12, **p3)
+        ndnwifi.net.mobility(b, 'stop', time=22, **p4)
+        ndnwifi.net.stopMobility(time=23)
+        ndnwifi.startMobility(time=0, mob_rep=1, reverse=False)
+
+    ndnwifi.start()
+    info("Starting NFD")
+    sleep(2)
+    nfds = AppManager(ndnwifi, ndnwifi.net.stations, Nfd)
+
+    info("Starting pingserver...")
+    ping_server_proc = getPopen(b, "ndnpingserver /example")
+    Nfdc.createFace(a, b.IP())
+    Nfdc.registerRoute(a, "/example", b.IP())
+
+    info("Starting ping...")
+    NDNPingClient.ping(a, "/example", 10)
+
+    # Start the CLI
+    MiniNDNWifiCLI(ndnwifi.net)
+    ndnwifi.net.stop()
+    ndnwifi.cleanUp()
+
+if __name__ == '__main__':
+    try:
+        runExperiment()
+    except Exception as e:
+        MinindnWifi.handleException()
\ No newline at end of file
diff --git a/install.sh b/install.sh
index 08255cf..e2d9343 100755
--- a/install.sh
+++ b/install.sh
@@ -192,6 +192,22 @@
     popd
 }
 
+function wifi {
+    if [[ updated != true ]]; then
+        $update
+        updated="true"
+    fi
+
+    if [[ $pysetup != true ]]; then
+        pysetup="true"
+    fi
+
+    git clone --depth 1 https://github.com/intrig-unicamp/mininet-wifi.git
+    pushd mininet-wifi
+    sudo ./util/install.sh -Wlfnv
+    popd
+}
+
 function infoedit {
     git clone --depth 1 https://github.com/NDN-Routing/infoedit.git $NDN_SRC/infoedit
     pushd $NDN_SRC/infoedit
@@ -225,6 +241,7 @@
     sudo cp topologies/current-testbed.conf "$install_dir"
     sudo cp topologies/geo_hyperbolic_test.conf "$install_dir"
     sudo cp topologies/geant.conf "$install_dir"
+    sudo cp topologies/wifi/singleap-topology.conf "$install_dir"
     sudo python setup.py develop
 }
 
@@ -347,25 +364,34 @@
 
     printf 'options:\n' >&2
     printf -- ' -a: install all the required dependencies\n' >&2
+    printf -- ' -A: install all the required dependencies (wired only)\n' >&2
     printf -- ' -c: install Common Client Libraries\n' >&2
     printf -- ' -d: build documentation\n' >&2
     printf -- ' -h: print this (H)elp message\n' >&2
     printf -- ' -i: install mini-ndn\n' >&2
-    printf -- ' -m: install mininet and dependencies\n' >&2
+    printf -- ' -m: install mininet and dependencies (for wired-only installation)\n' >&2
     printf -- ' -n: install NDN dependencies of mini-ndn including infoedit\n' >&2
     printf -- ' -p: patch ndn-cxx with dummy key chain\n' >&2
     printf -- ' -q: quiet install (must be specified first)\n' >&2
+    printf -- ' -w: install mininet-wifi and dependencies\n' >&2
     exit 2
 }
 
 if [[ $# -eq 0 ]]; then
     usage
 else
-    while getopts 'acdhimnpq' OPTION
+    while getopts 'aAcdhimnpqw' OPTION
     do
         case $OPTION in
         a)
         ndn
+        wifi
+        minindn
+        commonClientLibraries
+        break
+        ;;
+        A)
+        ndn
         mininet
         minindn
         commonClientLibraries
@@ -379,6 +405,7 @@
         n)    ndn;;
         p)    patchDummy;;
         q)    quiet_install;;
+        w)    wifi ;;
         ?)    usage;;
         esac
     done
diff --git a/minindn/helpers/ndnpingclient.py b/minindn/helpers/ndnpingclient.py
index 37b63db..a252a58 100644
--- a/minindn/helpers/ndnpingclient.py
+++ b/minindn/helpers/ndnpingclient.py
@@ -27,19 +27,21 @@
 
 class NDNPingClient(object):
     @staticmethod
-    def ping(source, dest, nPings=1, interval=None, timeout=None, starting_seq_num=None,
+    def ping(source, prefix, nPings=1, interval=None, timeout=None, starting_seq_num=None,
              identifier=None, allow_stale_data=False, print_timestamp=True, sleepTime=0.2):
-        print('Scheduling ping(s) from {} to {}'.format(source.name, dest.name))
+        print('Scheduling ping(s) from {} for {}'.format(source.name, prefix))
         # Use '&' to run in background and perform parallel pings
-        source.cmd('ndnping{1}{2}{3}{4}{5}{6}{7} /ndn/{0}-site/{0} >> ping-data/{0}.txt &'
+        source.cmd("mkdir ping-data")
+        source.cmd('ndnping{1}{2}{3}{4}{5}{6}{7} {0} >> ping-data/{8}.txt &'
         .format(
-            dest,
+            prefix,
             ' -c {}'.format(nPings),
             ' -i {}'.format(interval) if interval else '',
             ' -o {}'.format(timeout) if timeout  else '',
             ' -n {}'.format(starting_seq_num) if starting_seq_num else '',
             ' -p {}'.format(identifier) if identifier else '',
             ' -a' if allow_stale_data else '',
-            ' -t' if print_timestamp else ''
+            ' -t' if print_timestamp else '',
+            str.replace(prefix[1:], "/", "-")
         ))
         time.sleep(sleepTime)
diff --git a/minindn/helpers/nfdc.py b/minindn/helpers/nfdc.py
index 16dabb8..4fc9240 100644
--- a/minindn/helpers/nfdc.py
+++ b/minindn/helpers/nfdc.py
@@ -38,7 +38,7 @@
     @staticmethod
     def registerRoute(node, namePrefix, remoteNodeAddress, protocol=PROTOCOL_UDP, origin=255,
                       cost=0, inheritFlag=True, captureFlag=False, expirationInMillis=None):
-        cmd = ('nfdc route add {} {}://{} origin {} cost {} {}{}').format(
+        cmd = ('nfdc route add {} {}://{} origin {} cost {} {}{}{}').format(
             namePrefix,
             protocol,
             remoteNodeAddress,
diff --git a/minindn/minindn.py b/minindn/minindn.py
index 7b9c658..7a34787 100644
--- a/minindn/minindn.py
+++ b/minindn/minindn.py
@@ -29,13 +29,14 @@
 from subprocess import call, Popen, PIPE
 import shutil
 import glob
+from traceback import format_exc
 
 from mininet.topo import Topo
 from mininet.net import Mininet
 from mininet.link import TCLink
 from mininet.node import Switch
 from mininet.util import ipStr, ipParse
-from mininet.log import info, error
+from mininet.log import info, debug, error
 
 
 class Minindn(object):
@@ -232,3 +233,10 @@
         # sleep is not required if ndn-cxx is using in-memory keychain
         if not Minindn.ndnSecurityDisabled:
             time.sleep(seconds)
+
+    @staticmethod
+    def handleException():
+        'Utility method to perform cleanup steps and exit after catching exception'
+        Minindn.cleanUp()
+        info(format_exc())
+        exit(1)
\ No newline at end of file
diff --git a/minindn/util.py b/minindn/util.py
index 8584ffc..82fcdac 100644
--- a/minindn/util.py
+++ b/minindn/util.py
@@ -25,6 +25,7 @@
 from os.path import isfile
 from subprocess import call
 from mininet.cli import CLI
+from mn_wifi.cli import CLI as CLI_wifi
 
 sshbase = ['ssh', '-q', '-t', '-i/home/mininet/.ssh/id_rsa']
 scpbase = ['scp', '-i', '/home/mininet/.ssh/id_rsa']
@@ -75,3 +76,8 @@
     prompt = 'mini-ndn> '
     def __init__(self, mininet, stdin=sys.stdin, script=None):
         CLI.__init__(self, mininet, stdin, script)
+
+class MiniNDNWifiCLI(CLI_wifi):
+    prompt = 'mini-ndn-wifi> '
+    def __init__(self, mininet, stdin=sys.stdin, script=None):
+        CLI_wifi.__init__(self, mininet, stdin, script)
\ No newline at end of file
diff --git a/minindn/wifi/__init__.py b/minindn/wifi/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/minindn/wifi/__init__.py
diff --git a/minindn/wifi/minindnwifi.py b/minindn/wifi/minindnwifi.py
new file mode 100644
index 0000000..b2ca7f4
--- /dev/null
+++ b/minindn/wifi/minindnwifi.py
@@ -0,0 +1,207 @@
+# -*- Mode:python; c-file-style:"gnu"; indent-tabs-mode:nil -*- */
+#
+# Copyright (C) 2015-2020, The University of Memphis,
+#                          Arizona Board of Regents,
+#                          Regents of the University of California.
+#
+# This file is part of Mini-NDN.
+# See AUTHORS.md for a complete list of Mini-NDN authors and contributors.
+#
+# Mini-NDN is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Mini-NDN is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Mini-NDN, e.g., in COPYING.md file.
+# If not, see <http://www.gnu.org/licenses/>.
+
+import argparse
+import sys
+import time
+import os
+import configparser
+from subprocess import call, check_output, Popen
+from sys import exit
+
+from mininet.link import TCLink
+from mininet.node import Switch
+from mininet.util import ipStr, ipParse
+from mininet.log import info, debug
+
+from mn_wifi.topo import Topo as Topo_WiFi
+from mn_wifi.net import Mininet_wifi
+from mn_wifi.node import OVSKernelAP
+from mn_wifi.link import WirelessLink
+
+from minindn.minindn import Minindn
+
+class MinindnWifi(Minindn):
+    """ Class for handling default args, Mininet object and home directories """
+    def __init__(self, parser=argparse.ArgumentParser(), topo=None, topoFile=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)
+        mininetParams: Any params to pass to Mininet-WiFi
+        """
+        self.parser = self.parseArgs(parser)
+        self.args = self.parser.parse_args()
+
+        Minindn.workDir = self.args.workDir
+        Minindn.resultDir = self.args.resultDir
+
+        self.topoFile = None
+        if not topoFile:
+            # Args has default topology if none specified
+            self.topoFile = self.args.topoFile
+        else:
+            self.topoFile = topoFile
+
+        if topo is None:
+            try:
+                info('Using topology file {}\n'.format(self.topoFile))
+                self.topo = self.processTopo(self.topoFile)
+            except configparser.NoSectionError as e:
+                info('Error reading config file: {}\n'.format(e))
+                sys.exit(1)
+        else:
+            self.topo = topo
+
+        self.net = Mininet_wifi(topo=self.topo, ifb=self.args.ifb, link=WirelessLink, **mininetParams)
+
+        for host in self.net.stations:
+            if 'params' not in host.params:
+                host.params['params'] = {}
+            host.params['params']['workDir'] = Minindn.workDir
+            homeDir = "{}/{}".format(Minindn.workDir, host.name)
+            host.params['params']['homeDir'] = homeDir
+            debug(host.cmd("mkdir -p {}".format(homeDir)))
+            debug(host.cmd('export HOME={} && cd ~'.format(homeDir)))
+        try:
+            process = Popen(['ndnsec-get-default', '-k'], stdout=PIPE, stderr=PIPE)
+            output, error = process.communicate()
+            if process.returncode == 0:
+              Minindn.ndnSecurityDisabled = '/dummy/KEY/-%9C%28r%B8%AA%3B%60' in output
+              info('Dummy key chain patch is installed in ndn-cxx. Security will be disabled.\n')
+            else:
+              debug(error)
+        except:
+            pass
+
+        self.cleanups = []
+
+    @staticmethod
+    def parseArgs(parent):
+        parser = argparse.ArgumentParser(prog='minindn-wifi', parents=[parent], add_help=False)
+
+        # nargs='?' required here since optional argument
+        parser.add_argument('topoFile', nargs='?', default='/usr/local/etc/mini-ndn/singleap-topology.conf',
+                            help='If no template_file is given, topologies/wifi/singleap-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')
+
+        parser.add_argument('--mobility',action='store_true',dest='mobility',default=False,
+                            help='Enable custom mobility for topology (defined in topology file)')
+
+        parser.add_argument('--model-mob',action='store_true',dest='modelMob',default=False,
+                            help='Enable model mobility for topology (defined in topology file)')
+
+        parser.add_argument('--ifb',action='store_true',dest='ifb',default=False,
+                            help='Simulate delay on receiver-side by use of virtual IFB devices (see docs)')
+
+        return parser
+
+    @staticmethod
+    def processTopo(topoFile):
+        config = configparser.ConfigParser(delimiters=' ')
+        config.read(topoFile)
+        topo = Topo_WiFi()
+
+        items = config.items('stations')
+        debug("Stations")
+        for item in items:
+            debug(item[0].split(':'))
+            name = item[0].split(':')[0]
+            params = {}
+            for param in item[1].split(' '):
+                    if param == "_":
+                        continue
+                    key = param.split('=')[0]
+                    value = param.split('=')[1]
+                    if key in ['range']:
+                        value = int(value)
+                    params[key] = value
+
+            topo.addStation(name, **params)
+
+        try:
+            debug("Switches")
+            items = config.items('switches')
+            for item in items:
+                debug(item[0].split(':'))
+                name = item[0].split(':')[0]
+                topo.addSwitch(name)
+        except configparser.NoSectionError:
+            debug("Switches are optional")
+            pass
+
+        try:
+            debug("APs")
+            items = config.items('accessPoints')
+            for item in items:
+                debug(item[0].split(':'))
+                name = item[0].split(':')[0]
+                ap_params = {}
+                for param in item[1].split(' '):
+                    if param == "_":
+                        continue
+                    key = param.split('=')[0]
+                    value = param.split('=')[1]
+                    if key in ['range']:
+                        value = int(value)
+                    ap_params[key] = value
+                topo.addAccessPoint(name, **ap_params)
+        except configparser.NoSectionError:
+            debug("APs are optional")
+            pass
+
+        items = config.items('links')
+        debug("Links")
+        for item in items:
+            link = item[0].split(':')
+            debug(link)
+            params = {}
+            for param in item[1].split(' '):
+                if param == "_":
+                    continue
+                key = param.split('=')[0]
+                value = param.split('=')[1]
+                if key in ['bw', 'jitter', 'max_queue_size']:
+                    value = int(value)
+                if key == 'loss':
+                    value = float(value)
+                params[key] = value
+
+            topo.addLink(link[0], link[1], **params)
+
+        return topo
+
+    def startMobility(self, max_x=1000, max_y=1000, **kwargs):
+        """ Method to run a basic mobility setup on your net"""
+        self.net.plotGraph(max_x=max_x, max_y=max_y)
+        self.net.startMobility(**kwargs)
+
+    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
diff --git a/topologies/wifi/singleap-topology.conf b/topologies/wifi/singleap-topology.conf
new file mode 100644
index 0000000..191f6ca
--- /dev/null
+++ b/topologies/wifi/singleap-topology.conf
@@ -0,0 +1,11 @@
+[stations]
+sta1: range=10 speed=5
+sta2: range=50
+[cars]
+
+[accessPoints]
+ap1: position=50,50,50 range=50
+
+[links]
+sta1:ap1 delay=10ms
+sta2:ap1 delay=10ms
\ No newline at end of file