463 lines
18 KiB
Plaintext
Executable File
463 lines
18 KiB
Plaintext
Executable File
# -*- Mode:python; c-file-style:"gnu"; indent-tabs-mode:nil -*- */
|
|
#
|
|
# Copyright (C) 2015-2019, 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, error, warn
|
|
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.experiments.experiment import Experiment
|
|
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.nfd import Nfd
|
|
from ndn.util import ssh, scp, MiniNDNCLI, ProgramOptions
|
|
|
|
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
|
|
|
|
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)
|
|
|
|
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:
|
|
warn("Results directory ({}) already exists!".format(resultDir))
|
|
sys.exit(1)
|
|
|
|
info("Results will be stored at: {}".format(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():
|
|
error("No experiment named {}\n".format(options.experimentName))
|
|
sys.exit(1)
|
|
|
|
if options.experimentName is not None and options.resultDir is None:
|
|
warn("No results folder specified; experiment results will remain in the working directory\n")
|
|
|
|
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):
|
|
error("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:
|
|
error("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)
|
|
|
|
self.hosts_conf = parse_hosts(conf_arq)
|
|
self.switches_conf = parse_switches(conf_arq)
|
|
self.links_conf = parse_links(conf_arq)
|
|
|
|
self.isTCLink = False
|
|
self.isLimited = False
|
|
|
|
for host in self.hosts_conf:
|
|
if host.cpu != None and self.isLimited != True:
|
|
self.isLimited = True
|
|
self.addHost(host.name, app=host.app, params=host.params, cpu=host.cpu,
|
|
cores=host.cores,cache=host.cache, workdir=workDir)
|
|
|
|
for switch in self.switches_conf:
|
|
self.addSwitch(switch.name)
|
|
|
|
for link in self.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:
|
|
error('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:
|
|
error("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@{}".format(server)
|
|
src = nfdConfFile
|
|
dst = "{}:/tmp/nfd.conf".format(login)
|
|
scp(src, dst)
|
|
ssh(login, "sudo cp /tmp/nfd.conf {}".format(src))
|
|
|
|
if options.resultDir is not None:
|
|
options.resultDir = createResultsDir(options.resultDir, options.nFaces, options.routingType)
|
|
|
|
topo = NdnTopo(options.templateFile, options.workDir)
|
|
|
|
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)
|
|
|
|
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()
|
|
|
|
for host in net.hosts:
|
|
if 'app' in host.params:
|
|
if host.params['app'] != '':
|
|
app = host.params['app']
|
|
info("Starting {} on node {}".format(app, host.name))
|
|
info(host.cmd(app))
|
|
|
|
# Determine if each host is running NFD
|
|
for host in net.hosts:
|
|
nfdStatus = host.cmd("ps -g -U root | grep 'nfd --config {}/[n]fd.conf'".format(host.homeFolder))
|
|
if not host.nfd.isRunning or not nfdStatus:
|
|
error("NFD on host {} is not running. Printing log file and exiting...".format(host.name))
|
|
info(host.cmd("tail {}/nfd.log".format(host.homeFolder)))
|
|
net.stop()
|
|
sys.exit(1)
|
|
|
|
# Load experiment
|
|
experimentName = options.experimentName
|
|
|
|
experimentArgs = {
|
|
"net": net,
|
|
"options": options
|
|
}
|
|
|
|
if experimentName is not None:
|
|
info("Loading experiment: {}\n".format(experimentName))
|
|
|
|
experiment = ExperimentManager.create(experimentName, experimentArgs)
|
|
|
|
if experiment is not None:
|
|
experiment.start()
|
|
else:
|
|
error("Experiment '{}' does not exist\n".format(experimentName))
|
|
return
|
|
else:
|
|
experiment = Experiment(experimentArgs)
|
|
if options.isNlsrEnabled:
|
|
experiment.startNlsr(checkConvergence = False)
|
|
|
|
if options.isCliEnabled is True:
|
|
MiniNDNCLI(net)
|
|
|
|
net.stop()
|
|
|
|
if options.resultDir is not None:
|
|
info("Moving results to {}".format(options.resultDir))
|
|
for file in glob.glob('{}/*'.format(options.workDir)):
|
|
shutil.move(file, options.resultDir)
|
|
if options.cluster is not None:
|
|
for server in options.servers:
|
|
if server != "localhost":
|
|
login = "mininet@{}".format(server)
|
|
src = "{}:{}/*".format(login, options.workDir)
|
|
dst = options.resultDir
|
|
scp(src, dst)
|
|
info("Please clean work directories of other machines before running the cluster again")
|
|
|
|
def signal_handler(signal, frame):
|
|
info('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):
|
|
error("{} is missing from the system path! Exiting...".format(program))
|
|
sys.exit(1)
|
|
devnull.close()
|
|
|
|
if __name__ == '__main__':
|
|
|
|
signal.signal(signal.SIGQUIT, signal_handler)
|
|
|
|
options = parse_args()
|
|
|
|
setLogLevel('info')
|
|
verify_dependencies()
|
|
|
|
try:
|
|
execute(options)
|
|
except Exception as e:
|
|
error("{}".format(e))
|
|
call(["nfd-stop"])
|
|
call(["sudo", "mn", "--clean"])
|
|
sys.exit(1)
|