Implement ExperimentManager

refs: #2852

Change-Id: I49b50989477914ae4b076d6d39ca661f50fc92aa
diff --git a/.gitignore b/.gitignore
index be51b16..9b58941 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,4 @@
 openflow
 examples
 util
+.DS_Store
diff --git a/bin/minindn b/bin/minindn
index 97d3c74..e5305e9 100755
--- a/bin/minindn
+++ b/bin/minindn
@@ -7,9 +7,8 @@
 from mininet.link import TCLink
 from mininet.util import ipStr, ipParse
 
-from ndn.experiments.multiple_failure_experiment import MultipleFailureExperiment
-from ndn.experiments.pingall_experiment import PingallExperiment
-from ndn.experiments.failure_experiment import FailureExperiment
+
+from ndn import ExperimentManager
 from ndn.ndn_host import NdnHost, CpuLimitedNdnHost
 from ndn.conf_parser import parse_hosts, parse_links
 
@@ -17,10 +16,18 @@
 import optparse
 import datetime
 from os.path import expanduser
+import sys
 
 from ndn.nlsr import Nlsr, NlsrConfigGenerator
 from ndn.nfd import Nfd
 
+def printExperimentNames():
+    experimentNames = ExperimentManager.getExperimentNames()
+
+    print "Mini-NDN experiments:"
+    for experiment in experimentNames:
+        print "  %s" % experiment
+
 def parse_args():
     usage="""Usage: minindn [template_file] [ -t | --testbed ]
     If no template_file is given, ndn_utils/default-topology.conf (given sample file)
@@ -29,18 +36,21 @@
     """
 
     testbed = False
-    pingall = False
     hr = False
-    failure = False
-    isMultipleFailure = False
 
     parser = optparse.OptionParser(usage)
 
     parser.add_option("-t", "--testbed", action="store_true", dest="testbed",
     help="instantiates NDN Testbed")
 
-    parser.add_option("--pingall", action="store", dest="pingall", type="int",
-    help="Sequentiall pings all the other nodes from each node")
+    parser.add_option("--experiment", action="store", dest="experiment",
+    help="Runs the specified experiment")
+
+    parser.add_option("--list-experiments", action="store_true", dest="shouldListExperiments",
+    help="Lists the names of all available experiments")
+
+    parser.add_option("--nPings", action="store", dest="nPings", type="int",
+    help="Number of pings to perform between each node in the experiment")
 
     parser.add_option("--ctime", action="store", dest="ctime", type="int",
     help="Specify convergence time for the topology (Default 60 seconds)")
@@ -51,29 +61,30 @@
     parser.add_option("--faces", action="store", dest="faces", type="int",
     help="Specify number of faces 0-60")
 
-    parser.add_option("--failure", action="store_true", dest="failure",
-    help="Run failure experiment, specify the number of pings using pingall")
-
-    parser.add_option("--multiple-failure", action="store_true", dest="isMultipleFailure",
-    help="Run multiple failure experiment; each node will fail and recover once")
-
     parser.add_option("--no-cli", action="store_false", dest="isCliEnabled",
         help="Run experiments and exit without showing the command line interface")
 
     (options, arg) = parser.parse_args()
 
     testbed = options.testbed
-    pingall = options.pingall
+    experimentName = options.experiment
+    shouldListExperiments = options.shouldListExperiments
+    nPings = options.nPings
     ctime = options.ctime
     hr = options.hr
     faces = options.faces
-    failure = options.failure
-    isMultipleFailure = options.isMultipleFailure
     isCliEnabled = options.isCliEnabled
 
+    if shouldListExperiments is not None:
+        printExperimentNames()
+        sys.exit()
+
     if ctime is None:
         ctime = 60
 
+    if nPings is None:
+        nPings = 300
+
     if isCliEnabled is None:
         isCliEnabled = True
 
@@ -82,7 +93,7 @@
     else:
         file = arg[0]
 
-    return file, testbed, pingall, ctime, hr, faces, failure, isMultipleFailure, isCliEnabled
+    return file, testbed, experimentName, nPings, ctime, hr, faces, isCliEnabled
 
 class NdnTopo(Topo):
     def __init__(self, conf_arq, **opts):
@@ -110,7 +121,7 @@
 
         info('Parse of ' + conf_arq + ' done.\n')
 
-def execute(template_file='minindn.conf', testbed=False, pingall=None, ctime=None, hr=False, faces=3, failure=False, isMultipleFailure=False, isCliEnabled=True):
+def execute(template_file='minindn.conf', testbed=False, experimentName=None, nPings=None, ctime=None, hr=False, faces=3, isCliEnabled=True):
     "Create a network based on template_file"
 
     install_dir='/usr/local/etc/mini-ndn/'
@@ -187,30 +198,41 @@
 
     nodes = nodes[0:-1]
 
-    if isMultipleFailure is True:
-        test = MultipleFailureExperiment(net, nodes, ctime, Nfd.STRATEGY_BEST_ROUTE_V3)
-        test.start()
-    elif failure is True:
-        test = FailureExperiment(net, nodes, ctime, Nfd.STRATEGY_BEST_ROUTE_V3)
-        test.start()
-    elif pingall is not None:
-        test = PingallExperiment(net, nodes, ctime, pingall, Nfd.STRATEGY_BEST_ROUTE_V3)
-        test.start()
-
     for host in net.hosts:
         if 'app' in host.params:
             if host.params['app'] != '_':
                 host.cmd(host.params['app'])
 
+    # Load experiment
+    if experimentName is not None:
+        print "Loading experiment: %s" % experimentName
+
+        experimentArgs = {
+            "net": net,
+            "nodes": nodes,
+            "ctime": ctime,
+            "nPings": nPings,
+            "strategy": Nfd.STRATEGY_BEST_ROUTE_V3
+        }
+
+        experiment = ExperimentManager.create(experimentName, experimentArgs)
+
+        if experiment is not None:
+            experiment.start()
+        else:
+            print "ERROR: Experiment '%s' does not exist" % experimentName
+            return
+
     if isCliEnabled is True:
         CLI(net)
 
     net.stop()
 
 if __name__ == '__main__':
+
     hosts_conf = []
     links_conf = []
-    template, testbed, pingall, ctime, hr, faces, failure, isMultipleFailure, isCliEnabled = parse_args()
+    template, testbed, experimentName, nPings, ctime, hr, faces, isCliEnabled = parse_args()
 
     setLogLevel('info')
-    execute(template, testbed, pingall, ctime, hr, faces, failure, isMultipleFailure, isCliEnabled)
+    execute(template, testbed, experimentName, nPings, ctime, hr, faces, isCliEnabled)
diff --git a/ndn/__init__.py b/ndn/__init__.py
index e69de29..0f0d0e4 100644
--- a/ndn/__init__.py
+++ b/ndn/__init__.py
@@ -0,0 +1 @@
+import experiment_manager as ExperimentManager
\ No newline at end of file
diff --git a/ndn/experiment_manager.py b/ndn/experiment_manager.py
new file mode 100644
index 0000000..69dad29
--- /dev/null
+++ b/ndn/experiment_manager.py
@@ -0,0 +1,65 @@
+#!/usr/bin/python
+
+import os
+
+class _ExperimentManager:
+
+    class Error(Exception):
+        def __init__(self, what):
+            self.what = what
+        def __str__(self):
+            return repr(self.what)
+
+    instance = None
+
+    def __init__(self):
+        self.experiments = {}
+
+    def loadModules(self):
+        currentDir = os.path.dirname(__file__)
+        experimentDir = "%s/%s" % (currentDir, "experiments")
+        experimentModule = "ndn.experiments"
+
+        # Import and register experiments
+        for root, dirs, files in os.walk(experimentDir):
+            for filename in files:
+                if filename.endswith(".py") and filename != "__init__.py":
+                    module = filename.replace(".py", "")
+                    __import__("%s.%s" % (experimentModule, module))
+
+    def register(self, name, experimentClass):
+        if name not in self.experiments:
+            self.experiments[name] = experimentClass
+        else:
+            raise _ExperimentManager.Error("Experiment '%s' has already been registered" % name)
+
+    def create(self, name, args):
+        if name in self.experiments:
+            return self.experiments[name](args)
+        else:
+            return None
+
+def __getInstance():
+    if _ExperimentManager.instance is None:
+        _ExperimentManager.instance = _ExperimentManager()
+        _ExperimentManager.instance.loadModules()
+
+    return _ExperimentManager.instance
+
+def register(name, experimentClass):
+    manager = __getInstance()
+    manager.register(name, experimentClass)
+
+def create(name, args):
+    manager = __getInstance()
+    return manager.create(name, args)
+
+def getExperimentNames():
+    manager = __getInstance()
+
+    experimentNames = []
+
+    for key in manager.experiments:
+        experimentNames.append(key)
+
+    return experimentNames
diff --git a/ndn/experiments/experiment.py b/ndn/experiments/experiment.py
index 80fba81..d549828 100644
--- a/ndn/experiments/experiment.py
+++ b/ndn/experiments/experiment.py
@@ -3,14 +3,16 @@
 import time
 import sys
 
+from ndn import ExperimentManager
+
 class Experiment:
 
-    def __init__(self, net, nodes, convergenceTime, nPings, strategy):
-        self.net = net
-        self.nodes = nodes
-        self.convergenceTime = convergenceTime
-        self.nPings = nPings
-        self.strategy = strategy
+    def __init__(self, args):
+        self.net = args["net"]
+        self.nodes = args["nodes"]
+        self.convergenceTime = args["ctime"]
+        self.nPings = args["nPings"]
+        self.strategy = args["strategy"]
 
     def start(self):
         self.setup()
@@ -71,3 +73,7 @@
                 if host.name != other.name:
                     self.ping(host, other, self.nPings)
 
+    @staticmethod
+    def register(name, experimentClass):
+        ExperimentManager.register(name, experimentClass)
+
diff --git a/ndn/experiments/failure_experiment.py b/ndn/experiments/failure_experiment.py
index d4e9380..bae70c1 100644
--- a/ndn/experiments/failure_experiment.py
+++ b/ndn/experiments/failure_experiment.py
@@ -7,9 +7,10 @@
 
 class FailureExperiment(Experiment):
 
-    def __init__(self, net, nodes, convergenceTime, strategy):
+    def __init__(self, args):
+        args["nPings"] = 300
+        Experiment.__init__(self, args)
 
-        Experiment.__init__(self, net, nodes, convergenceTime, 300, strategy)
         self.PING_COLLECTION_TIME_BEFORE_FAILURE = 60
         self.PING_COLLECTION_TIME_AFTER_RECOVERY = 90
 
@@ -40,3 +41,5 @@
 
         # Collect pings for more seconds after CSU is up
         time.sleep(self.PING_COLLECTION_TIME_AFTER_RECOVERY)
+
+Experiment.register("failure", FailureExperiment)
diff --git a/ndn/experiments/multiple_failure_experiment.py b/ndn/experiments/multiple_failure_experiment.py
index db7d536..27c372a 100644
--- a/ndn/experiments/multiple_failure_experiment.py
+++ b/ndn/experiments/multiple_failure_experiment.py
@@ -7,7 +7,7 @@
 
 class MultipleFailureExperiment(Experiment):
 
-    def __init__(self, net, nodes, convergenceTime, strategy):
+    def __init__(self, args):
 
         self.PING_COLLECTION_TIME_BEFORE_FAILURE = 60
 
@@ -15,10 +15,12 @@
         self.RECOVERY_INTERVAL = 60
 
         # This is the number of pings required to make it through the full experiment
-        nInitialPings = self.PING_COLLECTION_TIME_BEFORE_FAILURE + len(net.hosts)*(self.FAILURE_INTERVAL + self.RECOVERY_INTERVAL)
+        nInitialPings = self.PING_COLLECTION_TIME_BEFORE_FAILURE + len(args["net"].hosts)*(self.FAILURE_INTERVAL + self.RECOVERY_INTERVAL)
         print("Scheduling with %s initial pings" % nInitialPings)
 
-        Experiment.__init__(self, net, nodes, convergenceTime, nInitialPings, strategy)
+        args["nPings"] = nInitialPings
+
+        Experiment.__init__(self, args)
 
     def failNode(self, host):
         print("Bringing %s down" % host.name)
@@ -65,3 +67,5 @@
                     self.ping(host, other, nPings)
 
             time.sleep(self.RECOVERY_INTERVAL)
+
+Experiment.register("multiple-failure", MultipleFailureExperiment)
diff --git a/ndn/experiments/pingall_experiment.py b/ndn/experiments/pingall_experiment.py
index 3bbfc1f..28ad27f 100644
--- a/ndn/experiments/pingall_experiment.py
+++ b/ndn/experiments/pingall_experiment.py
@@ -6,9 +6,9 @@
 
 class PingallExperiment(Experiment):
 
-    def __init__(self, net, nodes, convergenceTime, nPings, strategy):
+    def __init__(self, args):
 
-        Experiment.__init__(self, net, nodes, convergenceTime, nPings, strategy)
+        Experiment.__init__(self, args)
         self.COLLECTION_PERIOD_BUFFER = 10
 
 
@@ -17,3 +17,5 @@
 
         # For pingall experiment sleep for the number of pings + some offset
         time.sleep(self.nPings + self.COLLECTION_PERIOD_BUFFER)
+
+Experiment.register("pingall", PingallExperiment)