| # -*- Mode:python; c-file-style:"gnu"; indent-tabs-mode:nil -*- */ |
| # |
| # Copyright (C) 2015-2017, 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/>. |
| # |
| # This file incorporates work covered by the following copyright and |
| # permission notice: |
| # |
| # Mininet 2.3.0d1 License |
| # |
| # Copyright (c) 2013-2016 Open Networking Laboratory |
| # Copyright (c) 2009-2012 Bob Lantz and The Board of Trustees of |
| # The Leland Stanford Junior University |
| # |
| # Original authors: Bob Lantz and Brandon Heller |
| # |
| # We are making Mininet available for public use and benefit with the |
| # expectation that others will use, modify and enhance the Software and |
| # contribute those enhancements back to the community. However, since we |
| # would like to make the Software available for broadest use, with as few |
| # restrictions as possible permission is hereby granted, free of charge, to |
| # any person obtaining a copy of this Software to deal in the Software |
| # under the copyrights without restriction, including without limitation |
| # the rights to use, copy, modify, merge, publish, distribute, sublicense, |
| # and/or sell copies of the Software, and to permit persons to whom the |
| # Software is furnished to do so, subject to the following conditions: |
| # |
| # The above copyright notice and this permission notice shall be included |
| # in all copies or substantial portions of the Software. |
| # |
| # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS |
| # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
| # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. |
| # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY |
| # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, |
| # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE |
| # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
| # |
| # The name and trademarks of copyright holder(s) may NOT be used in |
| # advertising or publicity pertaining to the Software or any derivatives |
| # without specific, written prior permission. |
| |
| from mininet.topo import Topo |
| from mininet.net import Mininet |
| from mininet.log import setLogLevel, output, info |
| from mininet.link import TCLink |
| from mininet.util import ipStr, ipParse |
| |
| from mininet.examples.cluster import MininetCluster, RoundRobinPlacer, ClusterCleanup |
| from mininet.examples.clustercli import ClusterCLI |
| |
| from ndn import ExperimentManager |
| from ndn.ndn_host import NdnHost, CpuLimitedNdnHost, RemoteNdnHost |
| from ndn.conf_parser import parse_hosts, parse_switches, parse_links |
| from ndn.remote_ndn_link import RemoteNdnLink, RemoteGRENdnLink |
| from ndn.placer import GuidedPlacer, PopulatePlacement |
| from ndn.util import ssh, scp, MiniNDNCLI |
| |
| import os.path, time |
| import shutil |
| import argparse |
| import datetime |
| from os.path import expanduser |
| import sys |
| import signal |
| from subprocess import call |
| import glob |
| from functools import partial |
| import re |
| |
| from ndn.nlsr import Nlsr, NlsrConfigGenerator |
| from ndn.nfd import Nfd |
| |
| try: |
| import argcomplete |
| except ImportError: |
| pass |
| |
| VERSION_NUMBER = "0.4.0" |
| INSTALL_DIR='/usr/local/etc/mini-ndn/' |
| |
| class PrintExperimentNames(argparse.Action): |
| def __init__(self, option_strings, dest, nargs=0, help=None): |
| super(PrintExperimentNames, self).__init__(option_strings=option_strings, dest=dest, nargs=nargs, help=help) |
| |
| def __call__(self, parser, namespace, values, option_string=None): |
| experimentNames = ExperimentManager.getExperimentNames() |
| |
| print("Mini-NDN experiments:") |
| for experiment in experimentNames: |
| print(" {}".format(experiment)) |
| |
| sys.exit(0) |
| |
| class ProgramOptions: |
| def __init__(self): |
| self.ctime = 60 |
| self.experimentName = None |
| self.nFaces = 3 |
| self.templateFile = "minindn.conf" |
| self.routingType = "link-state" |
| self.isNlsrEnabled = True |
| self.isCliEnabled = True |
| self.nlsrSecurity = False |
| self.nPings = 300 |
| self.testbed = False |
| self.workDir = "/tmp/minindn" |
| self.resultDir = None |
| self.pctTraffic = 1.0 |
| self.cluster = None |
| self.servers = None |
| self.guided = None |
| self.placer = None |
| self.tunnelType = None |
| self.faceType = "udp" |
| self.arguments = None |
| self.csSize = 65536 |
| |
| def createResultsDir(resultDir, faces, rType): |
| if faces == 0: |
| faces = "all" |
| |
| routingChoice = "/{}/".format(rType) |
| |
| resultDir = "{}/{}/faces-{}".format(resultDir, routingChoice, faces) |
| resultDir = os.path.abspath(resultDir) |
| |
| if not os.path.isdir(resultDir): |
| os.makedirs(resultDir) |
| else: |
| print("Results directory (%s) already exists!" % resultDir) |
| sys.exit(1) |
| |
| print("Results will be stored at: %s" % resultDir) |
| return resultDir |
| |
| def parse_args(): |
| parser = argparse.ArgumentParser(prog='minindn') |
| |
| # nargs='?' required here since optional argument |
| parser.add_argument('tempfile', nargs='?', default=INSTALL_DIR + 'default-topology.conf', |
| help="If no template_file is given, topologies/default-topology.conf (given sample file) will be used.") |
| |
| parser.add_argument("--ctime", type=int, default=60, |
| help="Specify convergence time for the topology (Default: 60 seconds)") |
| |
| parser.add_argument("--experiment", choices=[experiment for experiment in ExperimentManager.getExperimentNames()], |
| help="Runs the specified experiment") |
| |
| parser.add_argument("--faces", type=int, default=3, |
| help="Specify number of max faces per prefix for NLSR 0-60") |
| |
| parser.add_argument("--routing", dest="routingType", default='link-state', choices=['link-state', 'hr', 'dry'], |
| help="""choices for routing are 'link-state' for link state, 'hr' for hyperbolic, and 'dry' |
| to test hyperbolic routing and compare with link state. Default is link-state.""") |
| |
| parser.add_argument("--no-nlsr", action="store_false", dest="isNlsrEnabled", |
| help="Run mini-ndn without NLSR routing") |
| |
| parser.add_argument("--list-experiments", action=PrintExperimentNames, |
| help="Lists the names of all available experiments") |
| |
| parser.add_argument("--no-cli", action="store_false", dest="isCliEnabled", |
| help="Run experiments and exit without showing the command line interface") |
| |
| parser.add_argument("--nPings", type=int, default=300, |
| help="Number of pings to perform between each node in the experiment") |
| |
| # store_true stores default value of False |
| parser.add_argument("--nlsr-security", action="store_true", dest="nlsrSecurity", |
| help="Enables NLSR security") |
| |
| parser.add_argument("-t", "--testbed", action="store_true", dest="testbed", |
| help="Instantiates a snapshot of the NDN Testbed irrespective of the tempfile provided") |
| |
| 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("--pct-traffic", dest="pctTraffic", type=float, default=1.0, |
| help="Specify the percentage of nodes each node should ping") |
| |
| parser.add_argument('--version', '-V', action='version', version='%(prog)s ' + VERSION_NUMBER, |
| help='Displays version information') |
| |
| parser.add_argument("--cluster", metavar='localhost,server2,...', |
| help="Run cluster edition") |
| |
| parser.add_argument("--placement", default='guided', |
| choices=['roundRobin', 'guided']) |
| |
| parser.add_argument("--place-list", dest="placeList", |
| help="""Provide corresponding number of nodes (comma separated) to put on |
| each node respectively of --cluster when guided placement is used""") |
| |
| parser.add_argument("--tunnel-type", dest="tunnelType", default='ssh', |
| choices=['ssh', 'gre']) |
| |
| parser.add_argument("--face-type", dest='faceType', default='udp', choices=['udp', 'tcp']) |
| |
| parser.add_argument("--cs-size", dest='csSize', type=int, default=65536, |
| help="Set CS size in NFD's conf file") |
| |
| ExperimentManager.addExperimentArgs(parser) |
| |
| if "argcomplete" in sys.modules: |
| argcomplete.autocomplete(parser) |
| |
| args = parser.parse_args() |
| |
| options = ProgramOptions() |
| options.templateFile = args.tempfile |
| options.ctime = args.ctime |
| options.experimentName = args.experiment |
| options.nFaces = args.faces |
| options.routingType = args.routingType |
| options.isNlsrEnabled = args.isNlsrEnabled |
| options.isCliEnabled = args.isCliEnabled |
| options.nlsrSecurity = args.nlsrSecurity |
| options.nPings = args.nPings |
| |
| options.testbed = args.testbed |
| options.workDir = args.workDir |
| options.resultDir = args.resultDir |
| options.pctTraffic = args.pctTraffic |
| options.cluster = args.cluster |
| options.placement = args.placement |
| options.tunnelType = args.tunnelType |
| options.placeList = args.placeList |
| options.faceType = args.faceType |
| options.csSize = args.csSize |
| options.arguments = args |
| |
| if options.experimentName is not None and options.experimentName not in ExperimentManager.getExperimentNames(): |
| print("No experiment named %s" % options.experimentName) |
| sys.exit(1) |
| |
| if options.experimentName is not None and options.resultDir is None: |
| print("No results folder specified; experiment results will remain in the working directory") |
| |
| if options.cluster is not None: |
| servers = options.cluster.split(',') |
| for server in servers: |
| ClusterCleanup.add(server) |
| options.servers = servers |
| |
| if options.placement == "roundRobin": |
| options.placement = RoundRobinPlacer |
| elif options.placement == "guided": |
| if options.placeList is None or not re.match("^[0-9,]+$", options.placeList): |
| print("Please specify correctly how many nodes you want to place on each node!") |
| sys.exit(1) |
| else: |
| try: |
| options.placeList = map(int, options.placeList.split(",")) |
| except ValueError: |
| print("Please specify the nodes correctly, no comma at the beginning/end!") |
| sys.exit(1) |
| |
| PopulatePlacement(options.placeList) |
| options.placement = GuidedPlacer |
| |
| if options.tunnelType == "ssh": |
| options.tunnelType = RemoteNdnLink |
| else: |
| options.tunnelType = RemoteGRENdnLink |
| |
| return options |
| |
| class NdnTopo(Topo): |
| def __init__(self, conf_arq, workDir, **opts): |
| Topo.__init__(self, **opts) |
| |
| global hosts_conf |
| global links_conf |
| hosts_conf = parse_hosts(conf_arq) |
| switches_conf = parse_switches(conf_arq) |
| links_conf = parse_links(conf_arq) |
| |
| self.isTCLink = False |
| self.isLimited = False |
| |
| for host in hosts_conf: |
| if host.cpu != None and self.isLimited != True: |
| self.isLimited = True |
| self.addHost(host.name, app=host.app, params=host.uri_tuples, cpu=host.cpu, |
| cores=host.cores,cache=host.cache, workdir=workDir) |
| if (options.routingType != 'link-state' and (host.params.get('radius') is None |
| or host.params.get('angle') is None)): |
| info('Hyperbolic coordinates in topology file are either missing or misconfigured.\n' \ |
| 'Check that each node has one radius value and one or two angle value(s).\n') |
| sys.exit(1) |
| |
| for switch in switches_conf: |
| self.addSwitch(switch.name) |
| |
| for link in links_conf: |
| if len(link.linkDict) == 0: |
| self.addLink(link.h1, link.h2) |
| else: |
| self.addLink(link.h1, link.h2, **link.linkDict) |
| self.isTCLink = True |
| |
| info('Parse of ' + conf_arq + ' done.\n') |
| |
| def execute(options): |
| "Create a network based on template_file" |
| |
| if options.testbed: |
| options.templateFile = INSTALL_DIR + 'minindn.testbed.conf' |
| |
| if os.path.exists(options.templateFile) == False: |
| info('Template file cannot be found. Exiting...\n') |
| sys.exit(1) |
| |
| if options.cluster is not None and options.placement == GuidedPlacer: |
| num_nodes = 0 |
| with open(options.templateFile, 'r') as topo: |
| for line in topo: |
| if ': _' in line: |
| num_nodes += 1 |
| |
| if sum(options.placeList) != num_nodes: |
| print("Placement list sum is not equal to number of nodes!") |
| sys.exit(1) |
| |
| # Copy nfd.conf to remote hosts - this assumes that NDN versions across |
| # the cluster are at least compatible if not the same |
| if options.cluster is not None: |
| for server in options.servers: |
| if server != "localhost": |
| login = "mininet@%s" % server |
| src = nfdConfFile |
| dst = "%s:/tmp/nfd.conf" % (login) |
| scp(src, dst) |
| ssh(login, "sudo cp /tmp/nfd.conf %s" % src) |
| |
| if options.resultDir is not None: |
| options.resultDir = createResultsDir(options.resultDir, options.nFaces, options.routingType) |
| |
| topo = NdnTopo(options.templateFile, options.workDir) |
| |
| t = datetime.datetime.now() |
| |
| if topo.isTCLink == True and topo.isLimited == True: |
| net = Mininet(topo,host=CpuLimitedNdnHost,link=TCLink) |
| elif topo.isTCLink == True and topo.isLimited == False: |
| if options.cluster is not None: |
| mn = partial(MininetCluster, servers=options.servers, placement=options.placement) |
| net = mn(topo=topo, host=RemoteNdnHost, link=options.tunnelType) |
| else: |
| net = Mininet(topo, host=NdnHost, link=TCLink) |
| elif topo.isTCLink == False and topo.isLimited == True: |
| net = Mininet(topo, host=CpuLimitedNdnHost) |
| else: |
| net = Mininet(topo, host=NdnHost) |
| |
| t2 = datetime.datetime.now() |
| |
| delta = t2 - t |
| |
| info('Setup time: ' + str(delta.seconds) + '\n') |
| |
| net.start() |
| |
| # Giving proper IPs to intf so neighbor nodes can communicate |
| # This is one way of giving connectivity, another way could be |
| # to insert a switch between each pair of neighbors |
| ndnNetBase = "1.0.0.0" |
| interfaces = [] |
| for host in net.hosts: |
| for intf in host.intfList(): |
| link = intf.link |
| node1, node2 = link.intf1.node, link.intf2.node |
| |
| if node1 in net.switches or node2 in net.switches: |
| continue |
| |
| if link.intf1 not in interfaces and link.intf2 not in interfaces: |
| interfaces.append(link.intf1) |
| interfaces.append(link.intf2) |
| node1.setIP(ipStr(ipParse(ndnNetBase) + 1) + '/30', intf=link.intf1) |
| node2.setIP(ipStr(ipParse(ndnNetBase) + 2) + '/30', intf=link.intf2) |
| ndnNetBase = ipStr(ipParse(ndnNetBase) + 4) |
| |
| time.sleep(2) |
| info('Starting NFD on nodes\n') |
| for host in net.hosts: |
| host.nfd = Nfd(host, options.csSize) |
| host.nfd.start() |
| |
| if options.isNlsrEnabled is True: |
| |
| # NLSR Security |
| if options.nlsrSecurity is True: |
| Nlsr.createKeysAndCertificates(net, options.workDir) |
| |
| # NLSR initialization |
| info('Starting NLSR on nodes\n') |
| for host in net.hosts: |
| conf = next(x for x in hosts_conf if x.name == host.name) |
| host.nlsrParameters = conf.nlsrParameters |
| |
| if options.nFaces is not None: |
| host.nlsrParameters["max-faces-per-prefix"] = options.nFaces |
| |
| if options.routingType == 'dry': |
| host.nlsrParameters["hyperbolic-state"] = "dry-run" |
| |
| elif options.routingType == 'hr': |
| host.nlsrParameters["hyperbolic-state"] = "on" |
| |
| # Generate NLSR configuration file |
| configGenerator = NlsrConfigGenerator(host, options.nlsrSecurity, options.faceType) |
| configGenerator.createConfigFile() |
| |
| # Start NLSR |
| host.nlsr = Nlsr(host, configGenerator.neighborIPs, options.faceType) |
| host.nlsr.start() |
| |
| for host in net.hosts: |
| if 'app' in host.params: |
| if host.params['app'] != '': |
| app = host.params['app'] |
| print("Starting " + app + " on node " + host.name) |
| print(host.cmd(app)) |
| |
| # Determine if each host is running NFD and NLSR |
| for host in net.hosts: |
| nfdStatus = host.cmd("ps -g -U root | grep 'nfd --config {}/[n]fd.conf'".format(host.homeFolder)) |
| nlsrStatus = host.cmd("ps -g | grep 'nlsr -f {}/[n]lsr.conf'".format(host.homeFolder)) |
| if not host.nfd.isRunning or not nfdStatus: |
| print("NFD on host {} is not running. Printing log file and exiting...".format(host.name)) |
| print(host.cmd("cat {}/nfd.log".format(host.homeFolder))) |
| net.stop() |
| sys.exit(1) |
| if (not host.nlsr.isRunning or not nlsrStatus) and options.isNlsrEnabled: |
| print("NLSR on host {} is not running. Printing log file and exiting...".format(host.name)) |
| print(host.cmd("cat {}/log/nlsr.log".format(host.homeFolder))) |
| net.stop() |
| sys.exit(1) |
| |
| # Load experiment |
| experimentName = options.experimentName |
| |
| if experimentName is not None: |
| print("Loading experiment: %s" % experimentName) |
| |
| experimentArgs = { |
| "net": net, |
| "ctime": options.ctime, |
| "nPings": options.nPings, |
| "strategy": Nfd.STRATEGY_BEST_ROUTE, |
| "pctTraffic": options.pctTraffic, |
| "nlsrSecurity": options.nlsrSecurity, |
| "workDir": options.workDir, |
| "arguments" : options.arguments |
| } |
| |
| experiment = ExperimentManager.create(experimentName, experimentArgs) |
| |
| if experiment is not None: |
| experiment.start() |
| else: |
| print("ERROR: Experiment '%s' does not exist" % experimentName) |
| return |
| |
| if options.isCliEnabled is True: |
| MiniNDNCLI(net) |
| |
| net.stop() |
| |
| if options.resultDir is not None: |
| print("Moving results to %s" % options.resultDir) |
| for file in glob.glob('%s/*' % options.workDir): |
| shutil.move(file, options.resultDir) |
| if options.cluster is not None: |
| for server in options.servers: |
| if server != "localhost": |
| login = "mininet@%s" % server |
| src = "%s:%s/*" % (login, options.workDir) |
| dst = options.resultDir |
| scp(src, dst) |
| print("Please clean work directories of other machines before running the cluster again") |
| |
| def signal_handler(signal, frame): |
| print('Cleaning up...') |
| call(["nfd-stop"]) |
| call(["sudo", "mn", "--clean"]) |
| sys.exit(1) |
| |
| def verify_dependencies(): |
| "Prevent MiniNDN from running without necessary dependencies" |
| dependencies = ["nfd", "nlsr", "infoedit", "ndnping", "ndnpingserver"] |
| devnull = open("/dev/null", "w") |
| # Checks that each program is in the system path |
| for program in dependencies: |
| if call(["which", program], stdout=devnull): |
| print("{} is missing from the system path! Exiting...".format(program)) |
| sys.exit(1) |
| devnull.close() |
| |
| if __name__ == '__main__': |
| |
| hosts_conf = [] |
| links_conf = [] |
| |
| signal.signal(signal.SIGQUIT, signal_handler) |
| |
| options = parse_args() |
| |
| setLogLevel('info') |
| verify_dependencies() |
| |
| try: |
| execute(options) |
| except Exception as e: |
| print("Error: {}".format(e)) |
| call(["nfd-stop"]) |
| call(["sudo", "mn", "--clean"]) |
| sys.exit(1) |