blob: 2e3b9ea888ae52a34d13997788cde4eb9ac8d3c0 [file] [log] [blame]
Alexander Laneea2d5d62019-10-04 16:48:52 -05001# -*- Mode:python; c-file-style:"gnu"; indent-tabs-mode:nil -*- */
2#
dulalsaurab20855442021-05-21 20:37:03 +00003# Copyright (C) 2015-2021, The University of Memphis,
Alexander Laneea2d5d62019-10-04 16:48:52 -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
awlanea169f572022-04-08 17:21:29 -050024import os
Alexander Laneea2d5d62019-10-04 16:48:52 -050025import argparse
26import sys
Alexander Laneea2d5d62019-10-04 16:48:52 -050027import configparser
dulalsaurab20855442021-05-21 20:37:03 +000028from subprocess import Popen, PIPE
Alexander Laneea2d5d62019-10-04 16:48:52 -050029
Alexander Laneea2d5d62019-10-04 16:48:52 -050030from mininet.log import info, debug
31
32from mn_wifi.topo import Topo as Topo_WiFi
33from mn_wifi.net import Mininet_wifi
Alexander Laneea2d5d62019-10-04 16:48:52 -050034from mn_wifi.link import WirelessLink
matianxing19921f07a832024-09-25 12:32:38 -050035from mn_wifi.link import wmediumd, adhoc
Alexander Laneea2d5d62019-10-04 16:48:52 -050036
37from minindn.minindn import Minindn
awlaned8e6b8e2022-05-16 23:49:56 -050038from minindn.helpers.nfdc import Nfdc
Alexander Laneea2d5d62019-10-04 16:48:52 -050039
matianxing19921f07a832024-09-25 12:32:38 -050040import ast
41
Alexander Laneea2d5d62019-10-04 16:48:52 -050042class MinindnWifi(Minindn):
Alex Lane407c5f02021-03-09 22:13:23 -060043 """ Class for handling default args, Mininet-wifi object and home directories """
awlaned8e6b8e2022-05-16 23:49:56 -050044 def __init__(self, parser=argparse.ArgumentParser(), topo=None, topoFile=None, noTopo=False,
45 link=WirelessLink, workDir=None, **mininetParams):
46 """
47 Create Mini-NDN-Wifi object
Alexander Laneea2d5d62019-10-04 16:48:52 -050048 parser: Parent parser of Mini-NDN-Wifi parser (use to specify experiment arguments)
49 topo: Mininet topo object (optional)
50 topoFile: topology file location (optional)
Alex Lane407c5f02021-03-09 22:13:23 -060051 noTopo: Allows specification of topology after network object is initialized (optional)
awlaned8e6b8e2022-05-16 23:49:56 -050052 link: Allows specification of default Mininet/Mininet-Wifi link type for
53 connections between nodes (optional)mininetParams: Any params to pass to Mininet-WiFi
Alexander Laneea2d5d62019-10-04 16:48:52 -050054 """
55 self.parser = self.parseArgs(parser)
56 self.args = self.parser.parse_args()
57
awlanea169f572022-04-08 17:21:29 -050058 if not workDir:
59 Minindn.workDir = os.path.abspath(self.args.workDir)
60 else:
61 Minindn.workDir = os.path.abspath(workDir)
62
Alexander Laneea2d5d62019-10-04 16:48:52 -050063 Minindn.resultDir = self.args.resultDir
64
65 self.topoFile = None
66 if not topoFile:
67 # Args has default topology if none specified
68 self.topoFile = self.args.topoFile
69 else:
70 self.topoFile = topoFile
71
awlaned8e6b8e2022-05-16 23:49:56 -050072 self.faces_to_create = {}
Alex Lane407c5f02021-03-09 22:13:23 -060073 if topo is None and not noTopo:
Alexander Laneea2d5d62019-10-04 16:48:52 -050074 try:
75 info('Using topology file {}\n'.format(self.topoFile))
awlaned8e6b8e2022-05-16 23:49:56 -050076 self.topo, self.faces_to_create = self.processTopo(self.topoFile)
Alexander Laneea2d5d62019-10-04 16:48:52 -050077 except configparser.NoSectionError as e:
78 info('Error reading config file: {}\n'.format(e))
79 sys.exit(1)
80 else:
81 self.topo = topo
82
Alex Lane407c5f02021-03-09 22:13:23 -060083 if not noTopo:
84 self.net = Mininet_wifi(topo=self.topo, ifb=self.args.ifb, link=link, **mininetParams)
85 else:
86 self.net = Mininet_wifi(ifb=self.args.ifb, link=link, **mininetParams)
Alexander Laneea2d5d62019-10-04 16:48:52 -050087
matianxing19921f07a832024-09-25 12:32:38 -050088 # process mobility if specified
89 if not noTopo:
90 self.processMobility(self.topoFile)
91
Alex Lane407c5f02021-03-09 22:13:23 -060092 # Prevents crashes running mixed topos
93 nodes = self.net.stations + self.net.hosts + self.net.cars
94 self.initParams(nodes)
95
Alexander Laneea2d5d62019-10-04 16:48:52 -050096 try:
97 process = Popen(['ndnsec-get-default', '-k'], stdout=PIPE, stderr=PIPE)
98 output, error = process.communicate()
99 if process.returncode == 0:
awlanec32a07b2022-04-19 14:53:41 -0500100 Minindn.ndnSecurityDisabled = '/dummy/KEY/-%9C%28r%B8%AA%3B%60' in output.decode("utf-8")
Alex Lane407c5f02021-03-09 22:13:23 -0600101 info('Dummy key chain patch is installed in ndn-cxx. Security will be disabled.\n')
Alexander Laneea2d5d62019-10-04 16:48:52 -0500102 else:
matianxing19921f07a832024-09-25 12:32:38 -0500103 debug(error + "\n")
Alexander Laneea2d5d62019-10-04 16:48:52 -0500104 except:
105 pass
106
107 self.cleanups = []
108
109 @staticmethod
110 def parseArgs(parent):
111 parser = argparse.ArgumentParser(prog='minindn-wifi', parents=[parent], add_help=False)
112
113 # nargs='?' required here since optional argument
114 parser.add_argument('topoFile', nargs='?', default='/usr/local/etc/mini-ndn/singleap-topology.conf',
115 help='If no template_file is given, topologies/wifi/singleap-topology.conf will be used.')
116
117 parser.add_argument('--work-dir', action='store', dest='workDir', default='/tmp/minindn',
118 help='Specify the working directory; default is /tmp/minindn')
119
120 parser.add_argument('--result-dir', action='store', dest='resultDir', default=None,
121 help='Specify the full path destination folder where experiment results will be moved')
122
123 parser.add_argument('--mobility',action='store_true',dest='mobility',default=False,
124 help='Enable custom mobility for topology (defined in topology file)')
125
Alexander Laneea2d5d62019-10-04 16:48:52 -0500126 parser.add_argument('--ifb',action='store_true',dest='ifb',default=False,
127 help='Simulate delay on receiver-side by use of virtual IFB devices (see docs)')
128
129 return parser
130
131 @staticmethod
matianxing19921f07a832024-09-25 12:32:38 -0500132 def convert_params(params):
133 converted_params = {}
134 for key, value in params.items():
135 try:
136 converted_params[key] = ast.literal_eval(value)
137 except (ValueError, SyntaxError):
138 converted_params[key] = value
139 return converted_params
140
141 @staticmethod
Alexander Laneea2d5d62019-10-04 16:48:52 -0500142 def processTopo(topoFile):
143 config = configparser.ConfigParser(delimiters=' ')
144 config.read(topoFile)
145 topo = Topo_WiFi()
146
147 items = config.items('stations')
matianxing19921f07a832024-09-25 12:32:38 -0500148 debug("Stations\n")
Alexander Laneea2d5d62019-10-04 16:48:52 -0500149 for item in items:
matianxing19921f07a832024-09-25 12:32:38 -0500150 debug(str(item[0].split(':'))+"\n")
Alexander Laneea2d5d62019-10-04 16:48:52 -0500151 name = item[0].split(':')[0]
152 params = {}
153 for param in item[1].split(' '):
154 if param == "_":
155 continue
156 key = param.split('=')[0]
157 value = param.split('=')[1]
158 if key in ['range']:
159 value = int(value)
160 params[key] = value
161
162 topo.addStation(name, **params)
163
164 try:
matianxing19921f07a832024-09-25 12:32:38 -0500165 debug("Switches\n")
Alexander Laneea2d5d62019-10-04 16:48:52 -0500166 items = config.items('switches')
167 for item in items:
matianxing19921f07a832024-09-25 12:32:38 -0500168 debug(str(item[0].split(':'))+"\n")
Alexander Laneea2d5d62019-10-04 16:48:52 -0500169 name = item[0].split(':')[0]
170 topo.addSwitch(name)
171 except configparser.NoSectionError:
matianxing19921f07a832024-09-25 12:32:38 -0500172 debug("Switches are optional\n")
Alexander Laneea2d5d62019-10-04 16:48:52 -0500173 pass
174
175 try:
176 debug("APs")
177 items = config.items('accessPoints')
178 for item in items:
matianxing19921f07a832024-09-25 12:32:38 -0500179 debug(str(item[0].split(':'))+"\n")
Alexander Laneea2d5d62019-10-04 16:48:52 -0500180 name = item[0].split(':')[0]
181 ap_params = {}
182 for param in item[1].split(' '):
183 if param == "_":
184 continue
185 key = param.split('=')[0]
186 value = param.split('=')[1]
187 if key in ['range']:
188 value = int(value)
189 ap_params[key] = value
190 topo.addAccessPoint(name, **ap_params)
191 except configparser.NoSectionError:
matianxing19921f07a832024-09-25 12:32:38 -0500192 debug("APs are optional\n")
Alexander Laneea2d5d62019-10-04 16:48:52 -0500193 pass
194
195 items = config.items('links')
196 debug("Links")
197 for item in items:
198 link = item[0].split(':')
matianxing19921f07a832024-09-25 12:32:38 -0500199 debug(str(link) + "\n")
Alexander Laneea2d5d62019-10-04 16:48:52 -0500200 params = {}
201 for param in item[1].split(' '):
202 if param == "_":
203 continue
204 key = param.split('=')[0]
205 value = param.split('=')[1]
Varun Patile427d262024-10-30 12:37:04 -0700206 if key in ['max_queue_size']:
Alexander Laneea2d5d62019-10-04 16:48:52 -0500207 value = int(value)
awlane5158e5f2024-11-01 21:34:14 -0500208 if key in ['loss', 'bw']:
Alexander Laneea2d5d62019-10-04 16:48:52 -0500209 value = float(value)
210 params[key] = value
211
212 topo.addLink(link[0], link[1], **params)
213
awlaned8e6b8e2022-05-16 23:49:56 -0500214 faces = {}
215 try:
216 items = config.items('faces')
matianxing19921f07a832024-09-25 12:32:38 -0500217 debug("Faces\n")
awlaned8e6b8e2022-05-16 23:49:56 -0500218 for item in items:
219 face_a, face_b = item[0].split(':')
matianxing19921f07a832024-09-25 12:32:38 -0500220 debug(str(item)+"\n")
awlaned8e6b8e2022-05-16 23:49:56 -0500221 cost = -1
222 for param in item[1].split(' '):
223 if param.split("=")[0] == 'cost':
224 cost = param.split("=")[1]
225 face_info = (face_b, int(cost))
226 if face_a not in faces:
227 faces[face_a] = [face_info]
228 else:
229 faces[face_a].append(face_info)
230 except configparser.NoSectionError:
matianxing19921f07a832024-09-25 12:32:38 -0500231 debug("Faces section is optional\n")
awlaned8e6b8e2022-05-16 23:49:56 -0500232 pass
233
234 return (topo, faces)
Alexander Laneea2d5d62019-10-04 16:48:52 -0500235
matianxing19921f07a832024-09-25 12:32:38 -0500236 def processMobility(self, topoFile):
237 config = configparser.ConfigParser(delimiters=' ')
238 config.read(topoFile)
239
240 try:
241 debug("Mobility\n")
242 items = config.items('mobility')
243 if len(items) == 0:
244 return
245 params = {}
246 for param in items[0][1].split(' '):
247 if param == "_":
248 continue
249 key = param.split('=')[0]
250 value = param.split('=')[1]
251 params[key] = value
252 params = self.convert_params(params)
253 self.startMobilityModel(**params)
254
255 items = config.items('stations')
256 debug("Start Mobility\n")
257 for item in items:
258 debug(str(item[0].split(':'))+"\n")
259 name = item[0].split(':')[0]
260 params = {}
261 for param in item[1].split(' '):
262 if param == "_":
263 continue
264 key = param.split('=')[0]
265 value = param.split('=')[1]
266 if key in ['range']:
267 value = int(value)
268 params[key] = value
269 # by default, nodes are moving
270 if "moving" not in params or params["moving"] == "True":
271 self.net.mobility(self.net[name],'start', time=0, **params)
272
273 except configparser.NoSectionError:
274 debug("Mobility section is optional\n")
275
Alexander Laneea2d5d62019-10-04 16:48:52 -0500276 def startMobility(self, max_x=1000, max_y=1000, **kwargs):
277 """ Method to run a basic mobility setup on your net"""
278 self.net.plotGraph(max_x=max_x, max_y=max_y)
279 self.net.startMobility(**kwargs)
280
281 def startMobilityModel(self, max_x=1000, max_y=1000, **kwargs):
282 """ Method to run a mobility model on your net until exited"""
283 self.net.plotGraph(max_x=max_x, max_y=max_y)
awlaned8e6b8e2022-05-16 23:49:56 -0500284 self.net.setMobilityModel(**kwargs)
285
286 def getWifiInterfaceDelay(self, node, interface=None):
287 """Method to return the configured tc delay of a wifi node's interface as a float"""
288 if not interface:
289 wifi_interface = "{}-wlan0".format(node.name)
290 else:
291 wifi_interface = interface
292 tc_output = node.cmd("tc qdisc show dev {}".format(wifi_interface))
293 for line in tc_output.splitlines():
294 if "qdisc netem 10:" in line:
295 split_line = line.split(" ")
296 for index in range(0, len(split_line)):
297 if split_line[index] == "delay":
298 return float(split_line[index + 1][:-2])
299 return 0.0
300
301 def setupFaces(self, faces_to_create=None):
302 """
303 Method to create unicast faces between nodes connected by an AP based on name or faces
304 between connected nodes; Returns dict- {node: (other node name, other node IP, other
305 node's delay as int)}. This is intended to pass to the NLSR helper via the faceDict param
306 """
307 if not faces_to_create:
308 faces_to_create = self.faces_to_create
309 # (nodeName, IP, delay as int)
310 # list of tuples
311 created_faces = dict()
312 batch_faces = dict()
313 for nodeAname in faces_to_create.keys():
314 if not nodeAname in batch_faces.keys():
315 batch_faces[nodeAname] = []
316 for nodeBname, faceCost in faces_to_create[nodeAname]:
317 if not nodeBname in batch_faces.keys():
318 batch_faces[nodeBname] = []
319 nodeA = self.net[nodeAname]
320 nodeB = self.net[nodeBname]
321 if nodeA.connectionsTo(nodeB):
322 best_interface = None
323 delay = None
324 for interface in nodeA.connectionsTo(nodeB):
325 interface_delay = self.getInterfaceDelay(nodeA, interface[0])
326 if not delay or int(interface_delay) < delay:
327 best_interface = interface
328 faceAIP = best_interface[0].IP()
329 faceBIP = best_interface[1].IP()
330 # Node delay should be symmetrical
331 nodeDelay = int(self.getInterfaceDelay(nodeA, best_interface[0]))
332 else:
333 # Default IP will be the primary wireless interface, unclear if multiple wireless
334 # interfaces should be handled
335 faceAIP = nodeA.IP()
336 faceBIP = nodeB.IP()
337 nodeADelay = self.getWifiInterfaceDelay(nodeA)
338 nodeBDelay = self.getWifiInterfaceDelay(nodeB)
339 nodeDelay = nodeADelay + nodeBDelay
340
341 if not faceCost == -1:
342 nodeALink = (nodeA.name, faceAIP, faceCost)
343 nodeBLink = (nodeB.name, faceBIP, faceCost)
344 else:
345 nodeALink = (nodeA.name, faceAIP, nodeDelay)
346 nodeBLink = (nodeB.name, faceBIP, nodeDelay)
347
348 batch_faces[nodeAname].append([faceBIP, "udp", True])
349 batch_faces[nodeBname].append([faceAIP, "udp", True])
350
351 if nodeA not in created_faces:
352 created_faces[nodeA] = [nodeBLink]
353 else:
354 created_faces[nodeA].append(nodeBLink)
355 if nodeB not in created_faces:
356 created_faces[nodeB] = [nodeALink]
357 else:
358 created_faces[nodeB].append(nodeALink)
359 for station_name in batch_faces.keys():
360 self.nfdcBatchProcessing(self.net[station_name], batch_faces[station_name])
matianxing19921f07a832024-09-25 12:32:38 -0500361 return created_faces
362
363class MinindnAdhoc(MinindnWifi):
364 """
365 Class for ad hoc network of Mininet-wifi
366 Topology example: topologies/wifi/adhoc-topology.conf
367 The link type is wmediumd by default
368 """
369 def __init__(self, parser=argparse.ArgumentParser(), topo=None, topoFile=None, noTopo=False,
370 link=wmediumd, workDir=None, **mininetParams):
371 # call parent constructor
372 super().__init__(parser, topo, topoFile, noTopo, link, workDir, **mininetParams)
373 if not self.topoFile:
374 info("Without topoFile, ad hoc links are not added to the network, and you need to"
375 " add them manually. The example topology file can be found in"
376 " topologies/wifi/adhoc-topology.conf\n")
377 else:
378 self.addAdhocLinks()
379
380 @staticmethod
381 def parseArgs(parent):
382 parser = argparse.ArgumentParser(prog='minindn-adhoc', parents=[parent], add_help=False)
383
384 # nargs='?' required here since optional argument
385 parser.add_argument('topoFile', nargs='?', default='/usr/local/etc/mini-ndn/adhoc-topology.conf',
386 help='If no template_file is given, topologies/wifi/adhoc-topology.conf will be used.')
387
388 parser.add_argument('--work-dir', action='store', dest='workDir', default='/tmp/minindn',
389 help='Specify the working directory; default is /tmp/minindn')
390
391 parser.add_argument('--result-dir', action='store', dest='resultDir', default=None,
392 help='Specify the full path destination folder where experiment results will be moved')
393
394 parser.add_argument('--mobility',action='store_true',dest='mobility',default=False,
395 help='Enable custom mobility for topology (defined in topology file)')
396
397 parser.add_argument('--ifb',action='store_true',dest='ifb',default=False,
398 help='Simulate delay on receiver-side by use of virtual IFB devices (see docs)')
399
400 return parser
401
402 @staticmethod
403 def processTopo(topoFile):
404 config = configparser.ConfigParser(delimiters=' ')
405 config.read(topoFile)
406 topo = Topo_WiFi()
407
408 items = config.items('stations')
409 debug("Stations\n")
410 id = 0
411 for item in items:
412 debug(str(item[0].split(':'))+"\n")
413 name = item[0].split(':')[0]
414 params = {}
415 for param in item[1].split(' '):
416 if param == "_":
417 continue
418 key = param.split('=')[0]
419 value = param.split('=')[1]
420 if key in ['range']:
421 value = int(value)
422 params[key] = value
423 # ip6 address for each station using id
424 if 'ip6' not in params:
425 params['ip6'] = 'fe80::{}'.format(id)
426 topo.addStation(name, **params)
427 id += 1
428
429 faces = {}
430 try:
431 items = config.items('faces')
432 debug("Faces\n")
433 for item in items:
434 face_a, face_b = item[0].split(':')
435 debug(str(item)+"\n")
436 cost = -1
437 for param in item[1].split(' '):
438 if param.split("=")[0] == 'cost':
439 cost = param.split("=")[1]
440 face_info = (face_b, int(cost))
441 if face_a not in faces:
442 faces[face_a] = [face_info]
443 else:
444 faces[face_a].append(face_info)
445 except configparser.NoSectionError:
446 debug("Faces section is optional\n")
447 pass
448
449 return (topo, faces)
450
451 """Add adhoc links to the network"""
452 # In the topo.py, all the links require two stations, but in adhoc topology, we need to add links for all the nodes.
453 def addAdhocLinks(self):
454 config = configparser.ConfigParser(delimiters=' ')
455 config.read(self.topoFile)
456
457 # read adhoc network parameters
458 # return if adhoc network is not defined
459 if 'adhocNetwork' not in config.sections():
460 info("Adhoc network is not defined in the topology file\n")
461 return
462
463 adhoc_params = config.items('adhocNetwork')
464 params = {}
465 for param in adhoc_params[0][1].split(' '):
466 if param == "_":
467 continue
468 key = param.split('=')[0]
469 value = param.split('=')[1]
470 if key in ['range']:
471 value = int(value)
472 params[key] = value
473 params = self.convert_params(params)
474
475 # return if ssid, mode, channel not defined in params
476 if 'ssid' not in params or 'mode' not in params or 'channel' not in params:
477 info("ssid, mode, channel not defined in adhoc network parameters\n")
478 return
479 networkParams = params
480
481 # add adhoc links
482 debug("Links\n")
483 items = config.items('stations')
484 for item in items:
485 debug(str(item[0].split(':'))+"\n")
486 name = item[0].split(':')[0]
487 params = {}
488 for param in item[1].split(' '):
489 if param == "_":
490 continue
491 key = param.split('=')[0]
492 value = param.split('=')[1]
493 params[key] = value
494 params = self.convert_params(params)
495 # replace | with space in bitrates because space is not allowed in the configuration file
496 if 'bitrates' in params:
497 params['bitrates'] = params['bitrates'].replace("|", " ")
498 self.net.addLink(name, cls=adhoc, intf='{}-wlan0'.format(name), **networkParams, **params)