#!/usr/bin/python

# Copyright (c)2011-2014 the Boeing Company.
# See the LICENSE file included in this distribution.
#
# author: Jeff Ahrenholz <jeffrey.m.ahrenholz@boeing.com>
#
"""
wlanemanetests.py - This script tests the performance of the WLAN device in
CORE by measuring various metrics:
    - delay experienced when pinging end-to-end
    - maximum TCP throughput achieved using iperf end-to-end
    - the CPU used and loss experienced when running an MGEN flow of UDP traffic

All MANET nodes are arranged in a row, so that any given node can only
communicate with the node to its right or to its left. Performance is measured
using traffic that travels across each hop in the network. Static /32 routing
is used instead of any dynamic routing protocol.

Various underlying network types are tested:
    - bridged (the CORE default, uses ebtables)
    - bridged with netem (add link effects to the bridge using tc queues)
    - EMANE bypass - the bypass model just forwards traffic
    - EMANE RF-PIPE - the bandwidth (bitrate) is set very high / no restrictions
    - EMANE RF-PIPE - bandwidth is set similar to netem case
    - EMANE RF-PIPE - default connectivity is off and pathloss events are
                      generated to connect the nodes in a line

Results are printed/logged in CSV format.

"""

import datetime
import math
import optparse
import os
import sys
import time

from core import emane
from core.emane.bypass import EmaneBypassModel
from core.emane.nodes import EmaneNode
from core.emane.rfpipe import EmaneRfPipeModel
from core.misc import ipaddress
from core.netns import nodes
from core.session import Session

try:
    import emaneeventservice
    import emaneeventpathloss
except Exception, e:
    try:
        from emanesh.events import EventService
        from emanesh.events import PathlossEvent
    except Exception, e2:
        raise ImportError("failed to import EMANE Python bindings:\n%s\n%s" % (e, e2))

# global Experiment object (for interaction with "python -i")
exp = None


# move these to core.misc.utils
def readstat():
    f = open("/proc/stat", "r")
    lines = f.readlines()
    f.close()
    return lines


def numcpus():
    lines = readstat()
    n = 0
    for l in lines[1:]:
        if l[:3] != "cpu":
            break
        n += 1
    return n


def getcputimes(line):
    # return (user, nice, sys, idle) from a /proc/stat cpu line
    # assume columns are:
    # cpu# user nice sys idle iowait irq softirq steal guest (man 5 proc)
    items = line.split()
    (user, nice, sys, idle) = map(lambda (x): int(x), items[1:5])
    return [user, nice, sys, idle]


def calculatecpu(timesa, timesb):
    for i in range(len(timesa)):
        timesb[i] -= timesa[i]
    total = sum(timesb)
    if total == 0:
        return 0.0
    else:
        # subtract % time spent in idle time
        return 100 - ((100.0 * timesb[-1]) / total)


# end move these to core.misc.utils

class Cmd(object):
    """ Helper class for running a command on a node and parsing the result. """
    args = ""

    def __init__(self, node, verbose=False):
        """ Initialize with a CoreNode (LxcNode) """
        self.id = None
        self.stdin = None
        self.out = None
        self.node = node
        self.verbose = verbose

    def info(self, msg):
        """ Utility method for writing output to stdout."""
        print msg
        sys.stdout.flush()

    def warn(self, msg):
        """ Utility method for writing output to stderr. """
        print >> sys.stderr, "XXX %s:" % self.node.name, msg
        sys.stderr.flush()

    def run(self):
        """ This is the primary method used for running this command. """
        self.open()
        status = self.id.wait()
        r = self.parse()
        self.cleanup()
        return r

    def open(self):
        """ Exceute call to node.popen(). """
        self.id, self.stdin, self.out, self.err = self.node.client.popen(self.args)

    def parse(self):
        """ This method is overloaded by child classes and should return some
            result.
        """
        return None

    def cleanup(self):
        """ Close the Popen channels."""
        self.stdin.close()
        self.out.close()
        self.err.close()
        tmp = self.id.wait()
        if tmp:
            self.warn("nonzero exit status:", tmp)


class ClientServerCmd(Cmd):
    """ Helper class for running a command on a node and parsing the result. """
    args = ""
    client_args = ""

    def __init__(self, node, client_node, verbose=False):
        """ Initialize with two CoreNodes, node is the server """
        Cmd.__init__(self, node, verbose)
        self.client_node = client_node

    def run(self):
        """ Run the server command, then the client command, then
        kill the server """
        self.open()  # server
        self.client_open()  # client
        status = self.client_id.wait()
        # stop the server
        self.node.cmd_output(["killall", self.args[0]])
        r = self.parse()
        self.cleanup()
        return r

    def client_open(self):
        """ Exceute call to client_node.popen(). """
        self.client_id, self.client_stdin, self.client_out, self.client_err = \
            self.client_node.client.popen(self.client_args)

    def parse(self):
        """ This method is overloaded by child classes and should return some
            result.
        """
        return None

    def cleanup(self):
        """ Close the Popen channels."""
        self.stdin.close()
        self.out.close()
        self.err.close()
        tmp = self.id.wait()
        if tmp:
            self.warn("nonzero exit status: %s" % tmp)
            self.warn("command was: %s" % (self.args,))


class PingCmd(Cmd):
    """ Test latency using ping.
    """

    def __init__(self, node, verbose=False, addr=None, count=50, interval=0.1, ):
        Cmd.__init__(self, node, verbose)
        self.addr = addr
        self.count = count
        self.interval = interval
        self.args = ["ping", "-q", "-c", "%s" % count, "-i", "%s" % interval, addr]

    def run(self):
        if self.verbose:
            self.info("%s initial test ping (max 1 second)..." % self.node.name)
        (status, result) = self.node.cmd_output(["ping", "-q", "-c", "1", "-w", "1", self.addr])
        if status != 0:
            self.warn("initial ping from %s to %s failed! result:\n%s" %
                      (self.node.name, self.addr, result))
            return 0.0, 0.0
        if self.verbose:
            self.info("%s pinging %s (%d seconds)..." %
                      (self.node.name, self.addr, self.count * self.interval))
        return Cmd.run(self)

    def parse(self):
        lines = self.out.readlines()
        avg_latency = 0
        mdev = 0
        try:
            stats_str = lines[-1].split("=")[1]
            stats = stats_str.split("/")
            avg_latency = float(stats[1])
            mdev = float(stats[3].split(" ")[0])
        except:
            self.warn("ping parsing exception: %s" % e)
        return avg_latency, mdev


class IperfCmd(ClientServerCmd):
    """ Test throughput using iperf.
    """

    def __init__(self, node, client_node, verbose=False, addr=None, time=10):
        # node is the server
        ClientServerCmd.__init__(self, node, client_node, verbose)
        self.addr = addr
        self.time = time
        # -s server, -y c CSV report output
        self.args = ["iperf", "-s", "-y", "c"]
        self.client_args = ["iperf", "-c", self.addr, "-t", "%s" % self.time]

    def run(self):
        if self.verbose:
            self.info("Launching the iperf server on %s..." % self.node.name)
            self.info("Running the iperf client on %s (%s seconds)..." % \
                      (self.client_node.name, self.time))
        return ClientServerCmd.run(self)

    def parse(self):
        lines = self.out.readlines()
        try:
            bps = int(lines[-1].split(",")[-1].strip("\n"))
        except Exception, e:
            self.warn("iperf parsing exception: %s" % e)
            bps = 0
        return bps


class MgenCmd(ClientServerCmd):
    """ Run a test traffic flow using an MGEN sender and receiver.
    """

    def __init__(self, node, client_node, verbose=False, addr=None, time=10,
                 rate=512):
        ClientServerCmd.__init__(self, node, client_node, verbose)
        self.addr = addr
        self.time = time
        self.args = ["mgen", "event", "listen udp 5000", "output",
                     "/var/log/mgen.log"]
        self.rate = rate
        sendevent = "ON 1 UDP DST %s/5000 PERIODIC [%s]" % \
                    (addr, self.mgenrate(self.rate))
        stopevent = "%s OFF 1" % time
        self.client_args = ["mgen", "event", sendevent, "event", stopevent,
                            "output", "/var/log/mgen.log"]

    @staticmethod
    def mgenrate(kbps):
        """ Return a MGEN periodic rate string for the given kilobits-per-sec.
            Assume 1500 byte MTU, 20-byte IP + 8-byte UDP headers, leaving
            1472 bytes for data.
        """
        bps = (kbps / 8) * 1000.0
        maxdata = 1472
        pps = math.ceil(bps / maxdata)
        return "%s %s" % (pps, maxdata)

    def run(self):
        if self.verbose:
            self.info("Launching the MGEN receiver on %s..." % self.node.name)
            self.info("Running the MGEN sender on %s (%s seconds)..." % \
                      (self.client_node.name, self.time))
        return ClientServerCmd.run(self)

    def cleanup(self):
        """ Close the Popen channels."""
        self.stdin.close()
        self.out.close()
        self.err.close()
        # non-zero mgen exit status OK
        tmp = self.id.wait()

    def parse(self):
        """ Check MGEN receiver"s log file for packet sequence numbers, and
            return the percentage of lost packets.
        """
        logfile = os.path.join(self.node.nodedir, "var.log/mgen.log")
        f = open(logfile, "r")
        numlost = 0
        lastseq = 0
        for line in f.readlines():
            fields = line.split()
            if fields[1] != "RECV":
                continue
            try:
                seq = int(fields[4].split(">")[1])
            except:
                self.info("Unexpected MGEN line:\n%s" % fields)
            if seq > (lastseq + 1):
                numlost += seq - (lastseq + 1)
            lastseq = seq
        f.close()
        if lastseq > 0:
            loss = 100.0 * numlost / lastseq
        else:
            loss = 0
        if self.verbose:
            self.info("Receiver log shows %d of %d packets lost" % \
                      (numlost, lastseq))
        return loss


class Experiment(object):
    """ Experiment object to organize tests.
    """

    def __init__(self, opt, start):
        """ Initialize with opt and start time. """
        self.session = None
        # node list
        self.nodes = []
        # WLAN network
        self.net = None
        self.verbose = opt.verbose
        # dict from OptionParser
        self.opt = opt
        self.start = start
        self.numping = opt.numping
        self.numiperf = opt.numiperf
        self.nummgen = opt.nummgen
        self.logbegin()

    def info(self, msg):
        """ Utility method for writing output to stdout. """
        print msg
        sys.stdout.flush()
        self.log(msg)

    def warn(self, msg):
        """ Utility method for writing output to stderr. """
        print >> sys.stderr, msg
        sys.stderr.flush()
        self.log(msg)

    def logbegin(self):
        """ Start logging. """
        self.logfp = None
        if not self.opt.logfile:
            return
        self.logfp = open(self.opt.logfile, "w")
        self.log("%s begin: %s\n" % (sys.argv[0], self.start.ctime()))
        self.log("%s args: %s\n" % (sys.argv[0], sys.argv[1:]))
        (sysname, rel, ver, machine, nodename) = os.uname()
        self.log("%s %s %s %s on %s" % (sysname, rel, ver, machine, nodename))

    def logend(self):
        """ End logging. """
        if not self.logfp:
            return
        end = datetime.datetime.now()
        self.log("%s end: %s (%s)\n" % \
                 (sys.argv[0], end.ctime(), end - self.start))
        self.logfp.flush()
        self.logfp.close()
        self.logfp = None

    def log(self, msg):
        """ Write to the log file, if any. """
        if not self.logfp:
            return
        print >> self.logfp, msg

    def reset(self):
        """ Prepare for another experiment run.
        """
        if self.session:
            self.session.shutdown()
            del self.session
            self.session = None
        self.nodes = []
        self.net = None

    def createbridgedsession(self, numnodes, verbose=False):
        """ Build a topology consisting of the given number of LxcNodes
            connected to a WLAN.
        """
        # IP subnet
        prefix = ipaddress.Ipv4Prefix("10.0.0.0/16")
        self.session = Session(1)
        # emulated network
        self.net = self.session.add_object(cls=nodes.WlanNode, name="wlan1")
        prev = None
        for i in xrange(1, numnodes + 1):
            addr = "%s/%s" % (prefix.addr(i), 32)
            tmp = self.session.add_object(cls=nodes.CoreNode, objid=i, name="n%d" % i)
            tmp.newnetif(self.net, [addr])
            self.nodes.append(tmp)
            self.session.services.add_services(tmp, "router", "IPForward")
            self.session.services.boot_services(tmp)
            self.staticroutes(i, prefix, numnodes)

            # link each node in a chain, with the previous node
            if prev:
                self.net.link(prev.netif(0), tmp.netif(0))
            prev = tmp

    def createemanesession(self, numnodes, verbose=False, cls=None, values=None):
        """ Build a topology consisting of the given number of LxcNodes
            connected to an EMANE WLAN.
        """
        prefix = ipaddress.Ipv4Prefix("10.0.0.0/16")
        self.session = Session(2)
        self.session.node_count = str(numnodes + 1)
        self.session.master = True
        self.session.location.setrefgeo(47.57917, -122.13232, 2.00000)
        self.session.location.refscale = 150.0
        self.session.emane.loadmodels()
        self.net = self.session.add_object(cls=EmaneNode, objid=numnodes + 1, name="wlan1")
        self.net.verbose = verbose
        # self.session.emane.addobj(self.net)
        for i in xrange(1, numnodes + 1):
            addr = "%s/%s" % (prefix.addr(i), 32)
            tmp = self.session.add_object(cls=nodes.CoreNode, objid=i,
                                          name="n%d" % i)
            # tmp.setposition(i * 20, 50, None)
            tmp.setposition(50, 50, None)
            tmp.newnetif(self.net, [addr])
            self.nodes.append(tmp)
            self.session.services.add_services(tmp, "router", "IPForward")

        if values is None:
            values = cls.getdefaultvalues()
        self.session.emane.setconfig(self.net.objid, cls.name, values)
        self.session.instantiate()

        self.info("waiting %s sec (TAP bring-up)" % 2)
        time.sleep(2)

        for i in xrange(1, numnodes + 1):
            tmp = self.nodes[i - 1]
            self.session.services.boot_services(tmp)
            self.staticroutes(i, prefix, numnodes)

    def setnodes(self):
        """ Set the sender and receiver nodes for use in this experiment,
            along with the address of the receiver to be used.
        """
        self.firstnode = self.nodes[0]
        self.lastnode = self.nodes[-1]
        self.lastaddr = self.lastnode.netif(0).addrlist[0].split("/")[0]

    def staticroutes(self, i, prefix, numnodes):
        """ Add static routes on node number i to the other nodes in the chain.
        """
        routecmd = ["/sbin/ip", "route", "add"]
        node = self.nodes[i - 1]
        neigh_left = ""
        neigh_right = ""
        # add direct interface routes first
        if i > 1:
            neigh_left = "%s" % prefix.addr(i - 1)
            cmd = routecmd + [neigh_left, "dev", node.netif(0).name]
            (status, result) = node.cmd_output(cmd)
            if status != 0:
                self.warn("failed to add interface route: %s" % cmd)
        if i < numnodes:
            neigh_right = "%s" % prefix.addr(i + 1)
            cmd = routecmd + [neigh_right, "dev", node.netif(0).name]
            (status, result) = node.cmd_output(cmd)
            if status != 0:
                self.warn("failed to add interface route: %s" % cmd)

        # add static routes to all other nodes via left/right neighbors
        for j in xrange(1, numnodes + 1):
            if abs(j - i) < 2:
                continue
            addr = "%s" % prefix.addr(j)
            if j < i:
                gw = neigh_left
            else:
                gw = neigh_right
            cmd = routecmd + [addr, "via", gw]
            (status, result) = node.cmd_output(cmd)
            if status != 0:
                self.warn("failed to add route: %s" % cmd)

    def setpathloss(self, numnodes):
        """ Send EMANE pathloss events to connect all NEMs in a chain.
        """
        if self.session.emane.version < self.session.emane.EMANE091:
            service = emaneeventservice.EventService()
            e = emaneeventpathloss.EventPathloss(1)
            old = True
        else:
            if self.session.emane.version == self.session.emane.EMANE091:
                dev = "lo"
            else:
                dev = self.session.obj("ctrlnet").brname
            service = EventService(eventchannel=("224.1.2.8", 45703, dev),
                                   otachannel=None)
            old = False

        for i in xrange(1, numnodes + 1):
            rxnem = i
            # inform rxnem that it can hear node to the left with 10dB noise
            txnem = rxnem - 1
            if txnem > 0:
                if old:
                    e.set(0, txnem, 10.0, 10.0)
                    service.publish(emaneeventpathloss.EVENT_ID,
                                    emaneeventservice.PLATFORMID_ANY, rxnem,
                                    emaneeventservice.COMPONENTID_ANY, e.export())
                else:
                    e = PathlossEvent()
                    e.append(txnem, forward=10.0, reverse=10.0)
                    service.publish(rxnem, e)
            # inform rxnem that it can hear node to the right with 10dB noise
            txnem = rxnem + 1
            if txnem > numnodes:
                continue
            if old:
                e.set(0, txnem, 10.0, 10.0)
                service.publish(emaneeventpathloss.EVENT_ID,
                                emaneeventservice.PLATFORMID_ANY, rxnem,
                                emaneeventservice.COMPONENTID_ANY, e.export())
            else:
                e = PathlossEvent()
                e.append(txnem, forward=10.0, reverse=10.0)
                service.publish(rxnem, e)

    def setneteffects(self, bw=None, delay=None):
        """ Set link effects for all interfaces attached to the network node.
        """
        if not self.net:
            self.warn("failed to set effects: no network node")
            return
        for netif in self.net.netifs():
            self.net.linkconfig(netif, bw=bw, delay=delay)

    def runalltests(self, title=""):
        """ Convenience helper to run all defined experiment tests.
            If tests are run multiple times, this returns the average of
            those runs.
        """
        duration = self.opt.duration
        rate = self.opt.rate
        if len(title) > 0:
            self.info("----- running %s tests (duration=%s, rate=%s) -----" % \
                      (title, duration, rate))
        (latency, mdev, throughput, cpu, loss) = (0, 0, 0, 0, 0)

        self.info("number of runs: ping=%d, iperf=%d, mgen=%d" % \
                  (self.numping, self.numiperf, self.nummgen))

        if self.numping > 0:
            (latency, mdev) = self.pingtest(count=self.numping)

        if self.numiperf > 0:
            throughputs = []
            for i in range(1, self.numiperf + 1):
                throughput = self.iperftest(time=duration)
                if self.numiperf > 1:
                    throughputs += throughput
                # iperf is very CPU intensive
                time.sleep(1)
            if self.numiperf > 1:
                throughput = sum(throughputs) / len(throughputs)
                self.info("throughputs=%s" % ["%.2f" % v for v in throughputs])

        if self.nummgen > 0:
            cpus = []
            losses = []
            for i in range(1, self.nummgen + 1):
                (cpu, loss) = self.cputest(time=duration, rate=rate)
                if self.nummgen > 1:
                    cpus += cpu,
                    losses += loss,
            if self.nummgen > 1:
                cpu = sum(cpus) / len(cpus)
                loss = sum(losses) / len(losses)
                self.info("cpus=%s" % ["%.2f" % v for v in cpus])
                self.info("losses=%s" % ["%.2f" % v for v in losses])

        return latency, mdev, throughput, cpu, loss

    def pingtest(self, count=50):
        """ Ping through a chain of nodes and report the average latency.
        """
        p = PingCmd(node=self.firstnode, verbose=self.verbose,
                    addr=self.lastaddr, count=count, interval=0.1).run()
        (latency, mdev) = p
        self.info("latency (ms): %.03f, %.03f" % (latency, mdev))
        return p

    def iperftest(self, time=10):
        """ Run iperf through a chain of nodes and report the maximum
            throughput.
        """
        bps = IperfCmd(node=self.lastnode, client_node=self.firstnode,
                       verbose=False, addr=self.lastaddr, time=time).run()
        self.info("throughput (bps): %s" % bps)
        return bps

    def cputest(self, time=10, rate=512):
        """ Run MGEN through a chain of nodes and report the CPU usage and
            percent of lost packets. Rate is in kbps.
        """
        if self.verbose:
            self.info("%s initial test ping (max 1 second)..." % \
                      self.firstnode.name)
        (status, result) = self.firstnode.cmd_output(["ping", "-q", "-c", "1",
                                                      "-w", "1", self.lastaddr])
        if status != 0:
            self.warn("initial ping from %s to %s failed! result:\n%s" % \
                      (self.firstnode.name, self.lastaddr, result))
            return 0.0, 0.0
        lines = readstat()
        cpustart = getcputimes(lines[0])
        loss = MgenCmd(node=self.lastnode, client_node=self.firstnode,
                       verbose=False, addr=self.lastaddr,
                       time=time, rate=rate).run()
        lines = readstat()
        cpuend = getcputimes(lines[0])
        percent = calculatecpu(cpustart, cpuend)
        self.info("CPU usage (%%): %.02f, %.02f loss" % (percent, loss))
        return percent, loss


def main():
    """ Main routine when running from command-line.
    """
    usagestr = "usage: %prog [-h] [options] [args]"
    parser = optparse.OptionParser(usage=usagestr)
    parser.set_defaults(numnodes=10, delay=3, duration=10, rate=512,
                        verbose=False,
                        numping=50, numiperf=1, nummgen=1)

    parser.add_option("-d", "--delay", dest="delay", type=float,
                      help="wait time before testing")
    parser.add_option("-l", "--logfile", dest="logfile", type=str,
                      help="log detailed output to the specified file")
    parser.add_option("-n", "--numnodes", dest="numnodes", type=int,
                      help="number of nodes")
    parser.add_option("-r", "--rate", dest="rate", type=float,
                      help="kbps rate to use for MGEN CPU tests")
    parser.add_option("--numping", dest="numping", type=int,
                      help="number of ping latency test runs")
    parser.add_option("--numiperf", dest="numiperf", type=int,
                      help="number of iperf throughput test runs")
    parser.add_option("--nummgen", dest="nummgen", type=int,
                      help="number of MGEN CPU tests runs")
    parser.add_option("-t", "--time", dest="duration", type=int,
                      help="duration in seconds of throughput and CPU tests")
    parser.add_option("-v", "--verbose", dest="verbose",
                      action="store_true", help="be more verbose")

    def usage(msg=None, err=0):
        sys.stdout.write("\n")
        if msg:
            sys.stdout.write(msg + "\n\n")
        parser.print_help()
        sys.exit(err)

    # parse command line opt
    (opt, args) = parser.parse_args()

    if opt.numnodes < 2:
        usage("invalid numnodes: %s" % opt.numnodes)
    if opt.delay < 0.0:
        usage("invalid delay: %s" % opt.delay)
    if opt.rate < 0.0:
        usage("invalid rate: %s" % opt.rate)

    for a in args:
        sys.stderr.write("ignoring command line argument: %s\n" % a)

        results = {}
        starttime = datetime.datetime.now()
        exp = Experiment(opt=opt, start=starttime)
        exp.info("Starting wlanemanetests.py tests %s" % starttime.ctime())

        # bridged
        exp.info("setting up bridged tests 1/2 no link effects")
        exp.info("creating topology: numnodes = %s" % (opt.numnodes,))
        exp.createbridgedsession(numnodes=opt.numnodes, verbose=opt.verbose)
        exp.setnodes()
        exp.info("waiting %s sec (node/route bring-up)" % opt.delay)
        time.sleep(opt.delay)
        results["0 bridged"] = exp.runalltests("bridged")
        exp.info("done; elapsed time: %s" % (datetime.datetime.now() - exp.start))

        # bridged with netem
        exp.info("setting up bridged tests 2/2 with netem")
        exp.setneteffects(bw=54000000, delay=0)
        exp.info("waiting %s sec (queue bring-up)" % opt.delay)
        results["1.0 netem"] = exp.runalltests("netem")
        exp.info("shutting down bridged session")

        # bridged with netem (1 Mbps,200ms)
        exp.info("setting up bridged tests 3/2 with netem")
        exp.setneteffects(bw=1000000, delay=20000)
        exp.info("waiting %s sec (queue bring-up)" % opt.delay)
        results["1.2 netem_1M"] = exp.runalltests("netem_1M")
        exp.info("shutting down bridged session")

        # bridged with netem (54 kbps,500ms)
        exp.info("setting up bridged tests 3/2 with netem")
        exp.setneteffects(bw=54000, delay=100000)
        exp.info("waiting %s sec (queue bring-up)" % opt.delay)
        results["1.4 netem_54K"] = exp.runalltests("netem_54K")
        exp.info("shutting down bridged session")
        exp.reset()

        # EMANE bypass model
        exp.info("setting up EMANE tests 1/2 with bypass model")
        exp.createemanesession(numnodes=opt.numnodes, verbose=opt.verbose, cls=EmaneBypassModel, values=None)
        exp.setnodes()
        exp.info("waiting %s sec (node/route bring-up)" % opt.delay)
        time.sleep(opt.delay)
        results["2.0 bypass"] = exp.runalltests("bypass")
        exp.info("shutting down bypass session")
        exp.reset()

        exp.info("waiting %s sec (between EMANE tests)" % opt.delay)
        time.sleep(opt.delay)

        # EMANE RF-PIPE model: no restrictions (max datarate)
        exp.info("setting up EMANE tests 2/4 with RF-PIPE model")
        rfpipevals = list(EmaneRfPipeModel.getdefaultvalues())
        rfpnames = EmaneRfPipeModel.getnames()
        # max value
        rfpipevals[rfpnames.index("datarate")] = "4294967295"
        if emanever < emane.EMANE091:
            rfpipevals[rfpnames.index("pathlossmode")] = "2ray"
            rfpipevals[rfpnames.index("defaultconnectivitymode")] = "1"
        else:
            rfpipevals[rfpnames.index("propagationmodel")] = "2ray"
        exp.createemanesession(numnodes=opt.numnodes, verbose=opt.verbose, cls=EmaneRfPipeModel, values=rfpipevals)
        exp.setnodes()
        exp.info("waiting %s sec (node/route bring-up)" % opt.delay)
        time.sleep(opt.delay)
        results["3.0 rfpipe"] = exp.runalltests("rfpipe")
        exp.info("shutting down RF-PIPE session")
        exp.reset()

        # EMANE RF-PIPE model: 54M datarate
        exp.info("setting up EMANE tests 3/4 with RF-PIPE model 54M")
        rfpipevals = list(EmaneRfPipeModel.getdefaultvalues())
        rfpnames = EmaneRfPipeModel.getnames()
        rfpipevals[rfpnames.index("datarate")] = "54000000"
        # TX delay != propagation delay
        # rfpipevals[ rfpnames.index("delay") ] = "5000"
        if emanever < emane.EMANE091:
            rfpipevals[rfpnames.index("pathlossmode")] = "2ray"
            rfpipevals[rfpnames.index("defaultconnectivitymode")] = "1"
        else:
            rfpipevals[rfpnames.index("propagationmodel")] = "2ray"
        exp.createemanesession(numnodes=opt.numnodes, verbose=opt.verbose,
                               cls=EmaneRfPipeModel, values=rfpipevals)
        exp.setnodes()
        exp.info("waiting %s sec (node/route bring-up)" % opt.delay)
        time.sleep(opt.delay)
        results["4.0 rfpipe54m"] = exp.runalltests("rfpipe54m")
        exp.info("shutting down RF-PIPE session")
        exp.reset()

        # EMANE RF-PIPE model:  54K datarate
        exp.info("setting up EMANE tests 4/4 with RF-PIPE model pathloss")
        rfpipevals = list(EmaneRfPipeModel.getdefaultvalues())
        rfpnames = EmaneRfPipeModel.getnames()
        rfpipevals[rfpnames.index("datarate")] = "54000"
        if emanever < emane.EMANE091:
            rfpipevals[rfpnames.index("pathlossmode")] = "pathloss"
            rfpipevals[rfpnames.index("defaultconnectivitymode")] = "0"
        else:
            rfpipevals[rfpnames.index("propagationmodel")] = "precomputed"
        exp.createemanesession(numnodes=opt.numnodes, verbose=opt.verbose,
                               cls=EmaneRfPipeModel, values=rfpipevals)
        exp.setnodes()
        exp.info("waiting %s sec (node/route bring-up)" % opt.delay)
        time.sleep(opt.delay)
        exp.info("sending pathloss events to govern connectivity")
        exp.setpathloss(opt.numnodes)
        results["5.0 pathloss"] = exp.runalltests("pathloss")
        exp.info("shutting down RF-PIPE session")
        exp.reset()

        # EMANE RF-PIPE model (512K, 200ms)
        exp.info("setting up EMANE tests 4/4 with RF-PIPE model pathloss")
        rfpipevals = list(EmaneRfPipeModel.getdefaultvalues())
        rfpnames = EmaneRfPipeModel.getnames()
        rfpipevals[rfpnames.index("datarate")] = "512000"
        rfpipevals[rfpnames.index("delay")] = "200"
        rfpipevals[rfpnames.index("pathlossmode")] = "pathloss"
        rfpipevals[rfpnames.index("defaultconnectivitymode")] = "0"
        exp.createemanesession(numnodes=opt.numnodes, verbose=opt.verbose,
                               cls=EmaneRfPipeModel, values=rfpipevals)
        exp.setnodes()
        exp.info("waiting %s sec (node/route bring-up)" % opt.delay)
        time.sleep(opt.delay)
        exp.info("sending pathloss events to govern connectivity")
        exp.setpathloss(opt.numnodes)
        results["5.1 pathloss"] = exp.runalltests("pathloss")
        exp.info("shutting down RF-PIPE session")
        exp.reset()

        # summary of results in CSV format
        exp.info("----- summary of results (%s nodes, rate=%s, duration=%s) -----" \
                 % (opt.numnodes, opt.rate, opt.duration))
        exp.info("netname:latency,mdev,throughput,cpu,loss")

        for test in sorted(results.keys()):
            (latency, mdev, throughput, cpu, loss) = results[test]
            exp.info("%s:%.03f,%.03f,%d,%.02f,%.02f" % \
                     (test, latency, mdev, throughput, cpu, loss))

        exp.logend()
        return exp


if __name__ == "__main__":
    main()