blob: 3d981bc75c4f097fa2db62eb242c1df174245e96 [file] [log] [blame]
# -*- Mode:python; c-file-style:"gnu"; indent-tabs-mode:nil -*- */
#
# Copyright (C) 2015-2018, 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 {}".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")
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: {}".format(experimentName))
experiment = ExperimentManager.create(experimentName, experimentArgs)
if experiment is not None:
experiment.start()
else:
error("Experiment '{}' does not exist".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)