blob: 6a6ad88b7569072e288e7dda8023aa9cca33fa52 [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
Alexander Laneea2d5d62019-10-04 16:48:52 -0500123 parser.add_argument('--ifb',action='store_true',dest='ifb',default=False,
124 help='Simulate delay on receiver-side by use of virtual IFB devices (see docs)')
125
126 return parser
127
128 @staticmethod
matianxing19921f07a832024-09-25 12:32:38 -0500129 def convert_params(params):
130 converted_params = {}
131 for key, value in params.items():
132 try:
133 converted_params[key] = ast.literal_eval(value)
134 except (ValueError, SyntaxError):
135 converted_params[key] = value
136 return converted_params
137
138 @staticmethod
Alexander Laneea2d5d62019-10-04 16:48:52 -0500139 def processTopo(topoFile):
140 config = configparser.ConfigParser(delimiters=' ')
141 config.read(topoFile)
142 topo = Topo_WiFi()
143
144 items = config.items('stations')
matianxing19921f07a832024-09-25 12:32:38 -0500145 debug("Stations\n")
Alexander Laneea2d5d62019-10-04 16:48:52 -0500146 for item in items:
matianxing19921f07a832024-09-25 12:32:38 -0500147 debug(str(item[0].split(':'))+"\n")
Alexander Laneea2d5d62019-10-04 16:48:52 -0500148 name = item[0].split(':')[0]
149 params = {}
150 for param in item[1].split(' '):
151 if param == "_":
152 continue
153 key = param.split('=')[0]
154 value = param.split('=')[1]
155 if key in ['range']:
156 value = int(value)
157 params[key] = value
158
159 topo.addStation(name, **params)
160
161 try:
matianxing19921f07a832024-09-25 12:32:38 -0500162 debug("Switches\n")
Alexander Laneea2d5d62019-10-04 16:48:52 -0500163 items = config.items('switches')
164 for item in items:
matianxing19921f07a832024-09-25 12:32:38 -0500165 debug(str(item[0].split(':'))+"\n")
Alexander Laneea2d5d62019-10-04 16:48:52 -0500166 name = item[0].split(':')[0]
167 topo.addSwitch(name)
168 except configparser.NoSectionError:
matianxing19921f07a832024-09-25 12:32:38 -0500169 debug("Switches are optional\n")
Alexander Laneea2d5d62019-10-04 16:48:52 -0500170 pass
171
172 try:
173 debug("APs")
174 items = config.items('accessPoints')
175 for item in items:
matianxing19921f07a832024-09-25 12:32:38 -0500176 debug(str(item[0].split(':'))+"\n")
Alexander Laneea2d5d62019-10-04 16:48:52 -0500177 name = item[0].split(':')[0]
178 ap_params = {}
179 for param in item[1].split(' '):
180 if param == "_":
181 continue
182 key = param.split('=')[0]
183 value = param.split('=')[1]
184 if key in ['range']:
185 value = int(value)
186 ap_params[key] = value
187 topo.addAccessPoint(name, **ap_params)
188 except configparser.NoSectionError:
matianxing19921f07a832024-09-25 12:32:38 -0500189 debug("APs are optional\n")
Alexander Laneea2d5d62019-10-04 16:48:52 -0500190 pass
191
192 items = config.items('links')
193 debug("Links")
194 for item in items:
195 link = item[0].split(':')
matianxing19921f07a832024-09-25 12:32:38 -0500196 debug(str(link) + "\n")
Alexander Laneea2d5d62019-10-04 16:48:52 -0500197 params = {}
198 for param in item[1].split(' '):
199 if param == "_":
200 continue
201 key = param.split('=')[0]
202 value = param.split('=')[1]
Varun Patile427d262024-10-30 12:37:04 -0700203 if key in ['max_queue_size']:
Alexander Laneea2d5d62019-10-04 16:48:52 -0500204 value = int(value)
awlane5158e5f2024-11-01 21:34:14 -0500205 if key in ['loss', 'bw']:
Alexander Laneea2d5d62019-10-04 16:48:52 -0500206 value = float(value)
207 params[key] = value
208
209 topo.addLink(link[0], link[1], **params)
210
awlaned8e6b8e2022-05-16 23:49:56 -0500211 faces = {}
212 try:
213 items = config.items('faces')
matianxing19921f07a832024-09-25 12:32:38 -0500214 debug("Faces\n")
awlaned8e6b8e2022-05-16 23:49:56 -0500215 for item in items:
216 face_a, face_b = item[0].split(':')
matianxing19921f07a832024-09-25 12:32:38 -0500217 debug(str(item)+"\n")
awlaned8e6b8e2022-05-16 23:49:56 -0500218 cost = -1
219 for param in item[1].split(' '):
220 if param.split("=")[0] == 'cost':
221 cost = param.split("=")[1]
222 face_info = (face_b, int(cost))
223 if face_a not in faces:
224 faces[face_a] = [face_info]
225 else:
226 faces[face_a].append(face_info)
227 except configparser.NoSectionError:
matianxing19921f07a832024-09-25 12:32:38 -0500228 debug("Faces section is optional\n")
awlaned8e6b8e2022-05-16 23:49:56 -0500229 pass
230
231 return (topo, faces)
Alexander Laneea2d5d62019-10-04 16:48:52 -0500232
matianxing19921f07a832024-09-25 12:32:38 -0500233 def processMobility(self, topoFile):
234 config = configparser.ConfigParser(delimiters=' ')
235 config.read(topoFile)
236
237 try:
238 debug("Mobility\n")
239 items = config.items('mobility')
240 if len(items) == 0:
241 return
242 params = {}
243 for param in items[0][1].split(' '):
244 if param == "_":
245 continue
246 key = param.split('=')[0]
247 value = param.split('=')[1]
248 params[key] = value
249 params = self.convert_params(params)
250 self.startMobilityModel(**params)
251
252 items = config.items('stations')
253 debug("Start Mobility\n")
254 for item in items:
255 debug(str(item[0].split(':'))+"\n")
256 name = item[0].split(':')[0]
257 params = {}
258 for param in item[1].split(' '):
259 if param == "_":
260 continue
261 key = param.split('=')[0]
262 value = param.split('=')[1]
263 if key in ['range']:
264 value = int(value)
265 params[key] = value
266 # by default, nodes are moving
267 if "moving" not in params or params["moving"] == "True":
268 self.net.mobility(self.net[name],'start', time=0, **params)
269
270 except configparser.NoSectionError:
271 debug("Mobility section is optional\n")
272
Alexander Laneea2d5d62019-10-04 16:48:52 -0500273 def startMobility(self, max_x=1000, max_y=1000, **kwargs):
274 """ Method to run a basic mobility setup on your net"""
275 self.net.plotGraph(max_x=max_x, max_y=max_y)
276 self.net.startMobility(**kwargs)
277
278 def startMobilityModel(self, max_x=1000, max_y=1000, **kwargs):
279 """ Method to run a mobility model on your net until exited"""
280 self.net.plotGraph(max_x=max_x, max_y=max_y)
awlaned8e6b8e2022-05-16 23:49:56 -0500281 self.net.setMobilityModel(**kwargs)
282
283 def getWifiInterfaceDelay(self, node, interface=None):
284 """Method to return the configured tc delay of a wifi node's interface as a float"""
285 if not interface:
286 wifi_interface = "{}-wlan0".format(node.name)
287 else:
288 wifi_interface = interface
289 tc_output = node.cmd("tc qdisc show dev {}".format(wifi_interface))
290 for line in tc_output.splitlines():
291 if "qdisc netem 10:" in line:
292 split_line = line.split(" ")
293 for index in range(0, len(split_line)):
294 if split_line[index] == "delay":
295 return float(split_line[index + 1][:-2])
296 return 0.0
297
298 def setupFaces(self, faces_to_create=None):
299 """
300 Method to create unicast faces between nodes connected by an AP based on name or faces
301 between connected nodes; Returns dict- {node: (other node name, other node IP, other
302 node's delay as int)}. This is intended to pass to the NLSR helper via the faceDict param
303 """
304 if not faces_to_create:
305 faces_to_create = self.faces_to_create
306 # (nodeName, IP, delay as int)
307 # list of tuples
308 created_faces = dict()
309 batch_faces = dict()
310 for nodeAname in faces_to_create.keys():
311 if not nodeAname in batch_faces.keys():
312 batch_faces[nodeAname] = []
313 for nodeBname, faceCost in faces_to_create[nodeAname]:
314 if not nodeBname in batch_faces.keys():
315 batch_faces[nodeBname] = []
316 nodeA = self.net[nodeAname]
317 nodeB = self.net[nodeBname]
318 if nodeA.connectionsTo(nodeB):
319 best_interface = None
320 delay = None
321 for interface in nodeA.connectionsTo(nodeB):
322 interface_delay = self.getInterfaceDelay(nodeA, interface[0])
323 if not delay or int(interface_delay) < delay:
324 best_interface = interface
325 faceAIP = best_interface[0].IP()
326 faceBIP = best_interface[1].IP()
327 # Node delay should be symmetrical
328 nodeDelay = int(self.getInterfaceDelay(nodeA, best_interface[0]))
329 else:
330 # Default IP will be the primary wireless interface, unclear if multiple wireless
331 # interfaces should be handled
332 faceAIP = nodeA.IP()
333 faceBIP = nodeB.IP()
334 nodeADelay = self.getWifiInterfaceDelay(nodeA)
335 nodeBDelay = self.getWifiInterfaceDelay(nodeB)
336 nodeDelay = nodeADelay + nodeBDelay
337
338 if not faceCost == -1:
339 nodeALink = (nodeA.name, faceAIP, faceCost)
340 nodeBLink = (nodeB.name, faceBIP, faceCost)
341 else:
342 nodeALink = (nodeA.name, faceAIP, nodeDelay)
343 nodeBLink = (nodeB.name, faceBIP, nodeDelay)
344
345 batch_faces[nodeAname].append([faceBIP, "udp", True])
346 batch_faces[nodeBname].append([faceAIP, "udp", True])
347
348 if nodeA not in created_faces:
349 created_faces[nodeA] = [nodeBLink]
350 else:
351 created_faces[nodeA].append(nodeBLink)
352 if nodeB not in created_faces:
353 created_faces[nodeB] = [nodeALink]
354 else:
355 created_faces[nodeB].append(nodeALink)
356 for station_name in batch_faces.keys():
357 self.nfdcBatchProcessing(self.net[station_name], batch_faces[station_name])
matianxing19921f07a832024-09-25 12:32:38 -0500358 return created_faces
359
360class MinindnAdhoc(MinindnWifi):
361 """
362 Class for ad hoc network of Mininet-wifi
363 Topology example: topologies/wifi/adhoc-topology.conf
364 The link type is wmediumd by default
365 """
366 def __init__(self, parser=argparse.ArgumentParser(), topo=None, topoFile=None, noTopo=False,
367 link=wmediumd, workDir=None, **mininetParams):
368 # call parent constructor
369 super().__init__(parser, topo, topoFile, noTopo, link, workDir, **mininetParams)
370 if not self.topoFile:
371 info("Without topoFile, ad hoc links are not added to the network, and you need to"
372 " add them manually. The example topology file can be found in"
373 " topologies/wifi/adhoc-topology.conf\n")
374 else:
375 self.addAdhocLinks()
376
377 @staticmethod
378 def parseArgs(parent):
379 parser = argparse.ArgumentParser(prog='minindn-adhoc', parents=[parent], add_help=False)
380
381 # nargs='?' required here since optional argument
382 parser.add_argument('topoFile', nargs='?', default='/usr/local/etc/mini-ndn/adhoc-topology.conf',
383 help='If no template_file is given, topologies/wifi/adhoc-topology.conf will be used.')
384
385 parser.add_argument('--work-dir', action='store', dest='workDir', default='/tmp/minindn',
386 help='Specify the working directory; default is /tmp/minindn')
387
388 parser.add_argument('--result-dir', action='store', dest='resultDir', default=None,
389 help='Specify the full path destination folder where experiment results will be moved')
390
matianxing19921f07a832024-09-25 12:32:38 -0500391 parser.add_argument('--ifb',action='store_true',dest='ifb',default=False,
392 help='Simulate delay on receiver-side by use of virtual IFB devices (see docs)')
393
394 return parser
395
396 @staticmethod
397 def processTopo(topoFile):
398 config = configparser.ConfigParser(delimiters=' ')
399 config.read(topoFile)
400 topo = Topo_WiFi()
401
402 items = config.items('stations')
403 debug("Stations\n")
404 id = 0
405 for item in items:
406 debug(str(item[0].split(':'))+"\n")
407 name = item[0].split(':')[0]
408 params = {}
409 for param in item[1].split(' '):
410 if param == "_":
411 continue
412 key = param.split('=')[0]
413 value = param.split('=')[1]
matianxing1992b95d1942025-01-28 15:04:02 -0600414 if key in ['range', 'min_v', 'max_v']:
matianxing19921f07a832024-09-25 12:32:38 -0500415 value = int(value)
416 params[key] = value
417 # ip6 address for each station using id
418 if 'ip6' not in params:
419 params['ip6'] = 'fe80::{}'.format(id)
420 topo.addStation(name, **params)
421 id += 1
422
423 faces = {}
424 try:
425 items = config.items('faces')
426 debug("Faces\n")
427 for item in items:
428 face_a, face_b = item[0].split(':')
429 debug(str(item)+"\n")
430 cost = -1
431 for param in item[1].split(' '):
432 if param.split("=")[0] == 'cost':
433 cost = param.split("=")[1]
434 face_info = (face_b, int(cost))
435 if face_a not in faces:
436 faces[face_a] = [face_info]
437 else:
438 faces[face_a].append(face_info)
439 except configparser.NoSectionError:
440 debug("Faces section is optional\n")
441 pass
442
443 return (topo, faces)
444
445 """Add adhoc links to the network"""
446 # In the topo.py, all the links require two stations, but in adhoc topology, we need to add links for all the nodes.
447 def addAdhocLinks(self):
448 config = configparser.ConfigParser(delimiters=' ')
449 config.read(self.topoFile)
450
451 # read adhoc network parameters
452 # return if adhoc network is not defined
453 if 'adhocNetwork' not in config.sections():
454 info("Adhoc network is not defined in the topology file\n")
455 return
456
457 adhoc_params = config.items('adhocNetwork')
458 params = {}
459 for param in adhoc_params[0][1].split(' '):
460 if param == "_":
461 continue
462 key = param.split('=')[0]
463 value = param.split('=')[1]
464 if key in ['range']:
465 value = int(value)
466 params[key] = value
467 params = self.convert_params(params)
468
469 # return if ssid, mode, channel not defined in params
470 if 'ssid' not in params or 'mode' not in params or 'channel' not in params:
471 info("ssid, mode, channel not defined in adhoc network parameters\n")
472 return
473 networkParams = params
474
475 # add adhoc links
476 debug("Links\n")
477 items = config.items('stations')
478 for item in items:
479 debug(str(item[0].split(':'))+"\n")
480 name = item[0].split(':')[0]
481 params = {}
482 for param in item[1].split(' '):
483 if param == "_":
484 continue
485 key = param.split('=')[0]
486 value = param.split('=')[1]
487 params[key] = value
488 params = self.convert_params(params)
489 # replace | with space in bitrates because space is not allowed in the configuration file
490 if 'bitrates' in params:
491 params['bitrates'] = params['bitrates'].replace("|", " ")
492 self.net.addLink(name, cls=adhoc, intf='{}-wlan0'.format(name), **networkParams, **params)