blob: 1ba1251a174c683bb4b33fd0da932900584be098 [file] [log] [blame]
Ashlesh Gawande6c86e302019-09-17 22:27:05 -05001# -*- Mode:python; c-file-style:"gnu"; indent-tabs-mode:nil -*- */
2#
Saurab Dulalb4286602021-06-11 12:10:14 -07003# Copyright (C) 2015-2021, The University of Memphis,
Ashlesh Gawande6c86e302019-09-17 22:27:05 -05004# Arizona Board of Regents,
5# Regents of the University of California.
6#
7# This file is part of Mini-NDN.
8# See AUTHORS.md for a complete list of Mini-NDN authors and contributors.
9#
10# Mini-NDN is free software: you can redistribute it and/or modify
11# it under the terms of the GNU General Public License as published by
12# the Free Software Foundation, either version 3 of the License, or
13# (at your option) any later version.
14#
15# Mini-NDN is distributed in the hope that it will be useful,
16# but WITHOUT ANY WARRANTY; without even the implied warranty of
17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18# GNU General Public License for more details.
19#
20# You should have received a copy of the GNU General Public License
21# along with Mini-NDN, e.g., in COPYING.md file.
22# If not, see <http://www.gnu.org/licenses/>.
23
24import argparse
25import sys
26import time
27import os
28import configparser
Saurab Dulal576a4192020-08-25 00:55:22 -050029from subprocess import call, Popen, PIPE
phmollad8d37e2020-03-03 12:53:08 +010030import shutil
31import glob
Alexander Laneea2d5d62019-10-04 16:48:52 -050032from traceback import format_exc
Ashlesh Gawande6c86e302019-09-17 22:27:05 -050033
34from mininet.topo import Topo
35from mininet.net import Mininet
36from mininet.link import TCLink
37from mininet.node import Switch
38from mininet.util import ipStr, ipParse
Alexander Laneea2d5d62019-10-04 16:48:52 -050039from mininet.log import info, debug, error
Ashlesh Gawande6c86e302019-09-17 22:27:05 -050040
41class Minindn(object):
Saurab Dulalb4286602021-06-11 12:10:14 -070042 """
43 This class provides the following features to the user:
Ashlesh Gawande6c86e302019-09-17 22:27:05 -050044 1) Wrapper around Mininet object with option to pass topology directly
45 1.1) Users can pass custom argument parser to extend the default on here
46 2) Parses the topology file given via command line if user does not pass a topology object
47 3) Provides way to stop Mini-NDN via stop
48 3.1) Applications register their clean up function with this class
49 4) Sets IPs on neighbors for connectivity required in a switch-less topology
50 5) Some other utility functions
51 """
52 ndnSecurityDisabled = False
Italo Valcyccd85b12020-07-24 12:35:20 -050053 workDir = '/tmp/minindn'
54 resultDir = None
Ashlesh Gawande6c86e302019-09-17 22:27:05 -050055
Saurab Dulalb4286602021-06-11 12:10:14 -070056 def __init__(self, parser=argparse.ArgumentParser(), topo=None, topoFile=None, noTopo=False,
awlanea169f572022-04-08 17:21:29 -050057 link=TCLink, workDir=None, **mininetParams):
Saurab Dulalb4286602021-06-11 12:10:14 -070058 """
59 Create MiniNDN object
60 :param parser: Parent parser of Mini-NDN parser
61 :param topo: Mininet topo object (optional)
62 :param topoFile: Mininet topology file location (optional)
63 :param noTopo: Allows specification of topology after network object is
64 initialized (optional)
65 :param link: Allows specification of default Mininet link type for connections between
66 nodes (optional)
67 :param mininetParams: Any params to pass to Mininet
Ashlesh Gawande6c86e302019-09-17 22:27:05 -050068 """
69 self.parser = Minindn.parseArgs(parser)
70 self.args = self.parser.parse_args()
71
awlanea169f572022-04-08 17:21:29 -050072 if not workDir:
73 Minindn.workDir = os.path.abspath(self.args.workDir)
74 else:
75 Minindn.workDir = os.path.abspath(workDir)
76
Italo Valcyccd85b12020-07-24 12:35:20 -050077 Minindn.resultDir = self.args.resultDir
Ashlesh Gawande6c86e302019-09-17 22:27:05 -050078
79 if not topoFile:
80 # Args has default topology if none specified
81 self.topoFile = self.args.topoFile
82 else:
83 self.topoFile = topoFile
84
awlaned8e6b8e2022-05-16 23:49:56 -050085 self.faces_to_create = {}
Alex Lane407c5f02021-03-09 22:13:23 -060086 if topo is None and not noTopo:
Ashlesh Gawande6c86e302019-09-17 22:27:05 -050087 try:
88 info('Using topology file {}\n'.format(self.topoFile))
awlaned8e6b8e2022-05-16 23:49:56 -050089 self.topo, self.faces_to_create = self.processTopo(self.topoFile)
Ashlesh Gawande6c86e302019-09-17 22:27:05 -050090 except configparser.NoSectionError as e:
91 info('Error reading config file: {}\n'.format(e))
92 sys.exit(1)
93 else:
94 self.topo = topo
95
Alex Lane407c5f02021-03-09 22:13:23 -060096 if not noTopo:
97 self.net = Mininet(topo=self.topo, link=link, **mininetParams)
98 else:
99 self.net = Mininet(link=link, **mininetParams)
Ashlesh Gawande6c86e302019-09-17 22:27:05 -0500100
Alex Lane407c5f02021-03-09 22:13:23 -0600101 self.initParams(self.net.hosts)
Ashlesh Gawande6c86e302019-09-17 22:27:05 -0500102
103 self.cleanups = []
104
105 if not self.net.switches:
106 self.ethernetPairConnectivity()
107
108 try:
Saurab Dulal576a4192020-08-25 00:55:22 -0500109 process = Popen(['ndnsec-get-default', '-k'], stdout=PIPE, stderr=PIPE)
110 output, error = process.communicate()
111 if process.returncode == 0:
awlanec32a07b2022-04-19 14:53:41 -0500112 Minindn.ndnSecurityDisabled = '/dummy/KEY/-%9C%28r%B8%AA%3B%60' in output.decode("utf-8")
Alex Lane407c5f02021-03-09 22:13:23 -0600113 info('Dummy key chain patch is installed in ndn-cxx. Security will be disabled.\n')
Saurab Dulal576a4192020-08-25 00:55:22 -0500114 else:
Alex Lane407c5f02021-03-09 22:13:23 -0600115 debug(error)
Ashlesh Gawande6c86e302019-09-17 22:27:05 -0500116 except:
117 pass
118
119 @staticmethod
120 def parseArgs(parent):
121 parser = argparse.ArgumentParser(prog='minindn', parents=[parent], add_help=False)
122
123 # nargs='?' required here since optional argument
124 parser.add_argument('topoFile', nargs='?', default='/usr/local/etc/mini-ndn/default-topology.conf',
awlaned8e6b8e2022-05-16 23:49:56 -0500125 help='If no template_file is given, topologies/default-topology.conf \
126 will be used.')
Ashlesh Gawande6c86e302019-09-17 22:27:05 -0500127
128 parser.add_argument('--work-dir', action='store', dest='workDir', default='/tmp/minindn',
129 help='Specify the working directory; default is /tmp/minindn')
130
131 parser.add_argument('--result-dir', action='store', dest='resultDir', default=None,
awlaned8e6b8e2022-05-16 23:49:56 -0500132 help='Specify the full path destination folder where experiment \
133 results will be moved')
Ashlesh Gawande6c86e302019-09-17 22:27:05 -0500134
135 return parser
136
137 def ethernetPairConnectivity(self):
138 ndnNetBase = '10.0.0.0'
139 interfaces = []
140 for host in self.net.hosts:
141 for intf in host.intfList():
142 link = intf.link
143 node1, node2 = link.intf1.node, link.intf2.node
144
145 if isinstance(node1, Switch) or isinstance(node2, Switch):
146 continue
147
148 if link.intf1 not in interfaces and link.intf2 not in interfaces:
149 interfaces.append(link.intf1)
150 interfaces.append(link.intf2)
151 node1.setIP(ipStr(ipParse(ndnNetBase) + 1) + '/30', intf=link.intf1)
152 node2.setIP(ipStr(ipParse(ndnNetBase) + 2) + '/30', intf=link.intf2)
153 ndnNetBase = ipStr(ipParse(ndnNetBase) + 4)
154
155 @staticmethod
156 def processTopo(topoFile):
Chad Cothraneef6ee82021-03-22 11:38:33 -0500157 config = configparser.ConfigParser(delimiters=' ', allow_no_value=True)
Ashlesh Gawande6c86e302019-09-17 22:27:05 -0500158 config.read(topoFile)
159 topo = Topo()
160
161 items = config.items('nodes')
dulalsaurab5c79db02020-02-27 06:04:56 +0000162 coordinates = []
163
Ashlesh Gawande6c86e302019-09-17 22:27:05 -0500164 for item in items:
165 name = item[0].split(':')[0]
Ashlesh Gawande6c86e302019-09-17 22:27:05 -0500166 params = {}
Saurab Dulalb4286602021-06-11 12:10:14 -0700167 if item[1]:
168 if all (x in item[1] for x in ['radius', 'angle']) and item[1] in coordinates:
169 error("FATAL: Duplicate Coordinate, \'{}\' used by multiple nodes\n" \
170 .format(item[1]))
171 sys.exit(1)
172 coordinates.append(item[1])
173
174 for param in item[1].split(' '):
175 if param == '_':
176 continue
177 params[param.split('=')[0]] = param.split('=')[1]
Ashlesh Gawande6c86e302019-09-17 22:27:05 -0500178
179 topo.addHost(name, params=params)
180
181 try:
182 items = config.items('switches')
183 for item in items:
184 name = item[0].split(':')[0]
185 topo.addSwitch(name)
186 except configparser.NoSectionError:
187 # Switches are optional
188 pass
189
190 items = config.items('links')
191 for item in items:
192 link = item[0].split(':')
193
194 params = {}
195 for param in item[1].split(' '):
196 key = param.split('=')[0]
197 value = param.split('=')[1]
awlanecb8c9112023-05-22 18:58:34 -0500198 if key in ['jitter', 'max_queue_size']:
Ashlesh Gawande6c86e302019-09-17 22:27:05 -0500199 value = int(value)
awlanecb8c9112023-05-22 18:58:34 -0500200 if key == 'loss' or key == 'bw':
Ashlesh Gawande6c86e302019-09-17 22:27:05 -0500201 value = float(value)
202 params[key] = value
203
204 topo.addLink(link[0], link[1], **params)
205
awlaned8e6b8e2022-05-16 23:49:56 -0500206 faces = {}
207 try:
208 items = config.items('faces')
209 debug("Faces")
210 for item in items:
211 face_a, face_b = item[0].split(':')
212 debug(item)
213 cost = -1
214 for param in item[1].split(' '):
215 if param.split("=")[0] == 'cost':
216 cost = param.split("=")[1]
217 face_info = (face_b, int(cost))
218 if face_a not in faces:
219 faces[face_a] = [face_info]
220 else:
221 faces[face_a].append(face_info)
222 except configparser.NoSectionError:
223 debug("Faces section is optional")
224 pass
225
226 return (topo, faces)
227
Ashlesh Gawande6c86e302019-09-17 22:27:05 -0500228 return topo
229
230 def start(self):
231 self.net.start()
232 time.sleep(3)
233
234 def stop(self):
235 for cleanup in self.cleanups:
236 cleanup()
237 self.net.stop()
238
Italo Valcyccd85b12020-07-24 12:35:20 -0500239 if Minindn.resultDir is not None:
240 info("Moving results to \'{}\'\n".format(Minindn.resultDir))
241 os.system("mkdir -p {}".format(Minindn.resultDir))
242 for file in glob.glob('{}/*'.format(Minindn.workDir)):
243 shutil.move(file, Minindn.resultDir)
phmollad8d37e2020-03-03 12:53:08 +0100244
Ashlesh Gawande6c86e302019-09-17 22:27:05 -0500245 @staticmethod
246 def cleanUp():
247 devnull = open(os.devnull, 'w')
248 call('nfd-stop', stdout=devnull, stderr=devnull)
249 call('mn --clean'.split(), stdout=devnull, stderr=devnull)
250
251 @staticmethod
252 def verifyDependencies():
253 """Prevent MiniNDN from running without necessary dependencies"""
254 dependencies = ['nfd', 'nlsr', 'infoedit', 'ndnping', 'ndnpingserver']
255 devnull = open(os.devnull, 'w')
256 # Checks that each program is in the system path
257 for program in dependencies:
258 if call(['which', program], stdout=devnull):
259 error('{} is missing from the system path! Exiting...\n'.format(program))
260 sys.exit(1)
261 devnull.close()
262
263 @staticmethod
264 def sleep(seconds):
265 # sleep is not required if ndn-cxx is using in-memory keychain
266 if not Minindn.ndnSecurityDisabled:
267 time.sleep(seconds)
Alexander Laneea2d5d62019-10-04 16:48:52 -0500268
269 @staticmethod
270 def handleException():
Saurab Dulalb4286602021-06-11 12:10:14 -0700271 """Utility method to perform cleanup steps and exit after catching exception"""
Alexander Laneea2d5d62019-10-04 16:48:52 -0500272 Minindn.cleanUp()
273 info(format_exc())
Alex Lane407c5f02021-03-09 22:13:23 -0600274 exit(1)
275
awlaned8e6b8e2022-05-16 23:49:56 -0500276 def getInterfaceDelay(self, node, interface):
277 tc_output = node.cmd("tc qdisc show dev {}".format(interface))
278 for line in tc_output.splitlines():
279 if "qdisc netem 10:" in line:
280 split_line = line.split(" ")
281 for index in range(0, len(split_line)):
282 if split_line[index] == "delay":
283 return float(split_line[index + 1][:-2])
284 return 0.0
285
Alex Lane407c5f02021-03-09 22:13:23 -0600286 def initParams(self, nodes):
Saurab Dulalb4286602021-06-11 12:10:14 -0700287 """Initialize Mini-NDN parameters for array of nodes"""
Alex Lane407c5f02021-03-09 22:13:23 -0600288 for host in nodes:
289 if 'params' not in host.params:
290 host.params['params'] = {}
291 host.params['params']['workDir'] = Minindn.workDir
292 homeDir = '{}/{}'.format(Minindn.workDir, host.name)
293 host.params['params']['homeDir'] = homeDir
294 host.cmd('mkdir -p {}'.format(homeDir))
awlaned8e6b8e2022-05-16 23:49:56 -0500295 host.cmd('export HOME={} && cd ~'.format(homeDir))
296
297 def nfdcBatchProcessing(self, station, faces):
298 # Input format: [IP, protocol, isPermanent]
299 batch_file = open("{}/{}/nfdc.batch".format(Minindn.workDir, station.name), "w")
300 lines = []
301 for face in faces:
302 ip = face[0]
303 protocol = face[1]
304 if face[2]:
305 face_type = "permanent"
306 else:
307 face_type = "persistent"
308 nfdc_command = "face create {}://{} {}\n".format(protocol, ip, face_type)
309 lines.append(nfdc_command)
310 batch_file.writelines(lines)
311 batch_file.close()
312 debug(station.cmd("nfdc -f {}/{}/nfdc.batch".format(Minindn.workDir, station.name)))
313
314 def setupFaces(self, faces_to_create=None):
315 """ Method to create unicast faces between connected nodes;
316 Returns dict- {node: (other node name, other node IP, other node's delay as int)}.
317 This is intended to pass to the NLSR helper via the faceDict param """
318 if not faces_to_create:
319 faces_to_create = self.faces_to_create
320 # (nodeName, IP, delay as int)
321 # list of tuples
322 created_faces = dict()
323 batch_faces = dict()
324 for nodeAname in faces_to_create.keys():
325 if not nodeAname in batch_faces.keys():
326 batch_faces[nodeAname] = []
327 for nodeBname, faceCost in faces_to_create[nodeAname]:
328 if not nodeBname in batch_faces.keys():
329 batch_faces[nodeBname] = []
330 nodeA = self.net[nodeAname]
331 nodeB = self.net[nodeBname]
332 if nodeA.connectionsTo(nodeB):
333 best_interface = None
334 delay = None
335 for interface in nodeA.connectionsTo(nodeB):
336 interface_delay = self.getInterfaceDelay(nodeA, interface[0])
337 if not delay or int(interface_delay) < delay:
338 best_interface = interface
339 faceAIP = best_interface[0].IP()
340 faceBIP = best_interface[1].IP()
341 # Node delay will be symmetrical for connected nodes
342 nodeDelay = int(self.getInterfaceDelay(nodeA, best_interface[0]))
343 #nodeBDelay = int(self.getInterfaceDelay(nodeB, best_interface[1]))
344 else:
345 # If no direct wired connections exist (ie when using a switch),
346 # assume the default interface
347 faceAIP = nodeA.IP()
348 faceBIP = nodeB.IP()
349 nodeADelay = int(self.getInterfaceDelay(nodeA, nodeA.defaultIntf()))
350 nodeBDelay = int(self.getInterfaceDelay(nodeB, nodeB.defaultIntf()))
351 nodeDelay = nodeADelay + nodeBDelay
352
353 if not faceCost == -1:
354 nodeALink = (nodeA.name, faceAIP, faceCost)
355 nodeBLink = (nodeB.name, faceBIP, faceCost)
356 else:
357 nodeALink = (nodeA.name, faceAIP, nodeDelay)
358 nodeBLink = (nodeB.name, faceBIP, nodeDelay)
359
360 # Importing this before initialization causes issues
361 batch_faces[nodeAname].append([faceBIP, "udp", True])
362 batch_faces[nodeBname].append([faceAIP, "udp", True])
363
364 if nodeA not in created_faces:
365 created_faces[nodeA] = [nodeBLink]
366 else:
367 created_faces[nodeA].append(nodeBLink)
368 if nodeB not in created_faces:
369 created_faces[nodeB] = [nodeALink]
370 else:
371 created_faces[nodeB].append(nodeALink)
372 for station_name in batch_faces.keys():
373 self.nfdcBatchProcessing(self.net[station_name], batch_faces[station_name])
374 return created_faces