2 # NEPI, a framework to manage network experiments
3 # Copyright (C) 2013 INRIA
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License version 2 as
7 # published by the Free Software Foundation;
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
17 # Author: Alina Quereilhac <alina.quereilhac@inria.fr>
26 SINGLETON = "singleton::"
27 SIMULATOR_UUID = "singleton::Simulator"
28 CONFIG_UUID = "singleton::Config"
29 GLOBAL_VALUE_UUID = "singleton::GlobalValue"
30 IPV4_GLOBAL_ROUTING_HELPER_UUID = "singleton::Ipv4GlobalRoutingHelper"
32 def load_ns3_libraries():
36 libdir = os.environ.get("NS3LIBRARIES")
38 # Load the ns-3 modules shared libraries
40 files = os.listdir(libdir)
41 regex = re.compile("(.*\.so)$")
42 libs = [m.group(1) for filename in files for m in [regex.search(filename)] if m]
44 initial_size = len(libs)
45 # Try to load the libraries in the right order by trial and error.
46 # Loop until all libraries are loaded.
49 libfile = os.path.join(libdir, lib)
51 ctypes.CDLL(libfile, ctypes.RTLD_GLOBAL)
55 #err = traceback.format_exc()
59 # if did not load any libraries in the last iteration break
60 # to prevent infinit loop
61 if initial_size == len(libs):
62 raise RuntimeError("Imposible to load shared libraries %s" % str(libs))
63 initial_size = len(libs)
65 def load_ns3_module():
68 # import the python bindings for the ns-3 modules
69 bindings = os.environ.get("NS3BINDINGS")
71 sys.path.append(bindings)
77 # create a Python module to add all ns3 classes
78 ns3mod = imp.new_module("ns3")
79 sys.modules["ns3"] = ns3mod
81 for importer, modname, ispkg in pkgutil.iter_modules(ns.__path__):
82 if modname in [ "visualizer" ]:
85 fullmodname = "ns.%s" % modname
86 module = __import__(fullmodname, globals(), locals(), ['*'])
88 for sattr in dir(module):
89 if sattr.startswith("_"):
92 attr = getattr(module, sattr)
94 # netanim.Config and lte.Config singleton overrides ns3::Config
95 if sattr == "Config" and modname in ['netanim', 'lte']:
96 sattr = "%s.%s" % (modname, sattr)
98 setattr(ns3mod, sattr, attr)
102 class NS3Wrapper(object):
103 def __init__(self, loglevel = logging.INFO, enable_dump = False):
104 super(NS3Wrapper, self).__init__()
105 # Thread used to run the simulation
106 self._simulation_thread = None
107 self._condition = None
109 # True if Simulator::Run was invoked
110 self._started = False
112 # holds reference to all C++ objects and variables in the simulation
113 self._objects = dict()
116 self._logger = logging.getLogger("ns3wrapper")
117 self._logger.setLevel(loglevel)
119 ## NOTE that the reason to create a handler to the ns3 module,
120 # that is re-loaded each time a ns-3 wrapper is instantiated,
121 # is that else each unit test for the ns3wrapper class would need
122 # a separate file. Several ns3wrappers would be created in the
123 # same unit test (single process), leading to inchorences in the
124 # state of ns-3 global objects
126 # Handler to ns3 classes
129 # Collection of allowed ns3 classes
130 self._allowed_types = None
132 # Object to dump instructions to reproduce and debug experiment
133 from nepi.resources.ns3.ns3wrapper_debug import NS3WrapperDebuger
134 self._debuger = NS3WrapperDebuger(enabled = enable_dump)
143 # load ns-3 libraries and bindings
144 self._ns3 = load_ns3_module()
149 def allowed_types(self):
150 if not self._allowed_types:
151 self._allowed_types = set()
152 type_id = self.ns3.TypeId()
154 tid_count = type_id.GetRegisteredN()
155 base = type_id.LookupByName("ns3::Object")
157 for i in range(tid_count):
158 tid = type_id.GetRegistered(i)
160 if tid.MustHideFromDocumentation() or \
161 not tid.HasConstructor() or \
162 not tid.IsChildOf(base):
165 type_name = tid.GetName()
166 self._allowed_types.add(type_name)
168 return self._allowed_types
175 def is_running(self):
176 return self.is_started and not self.ns3.Simulator.IsFinished()
179 def is_started(self):
180 if not self._started:
181 now = self.ns3.Simulator.Now()
188 def is_finished(self):
189 return self.ns3.Simulator.IsFinished()
192 return "uuid%s" % uuid.uuid4()
194 def get_object(self, uuid):
195 return self._objects.get(uuid)
197 def factory(self, type_name, **kwargs):
198 """ This method should be used to construct ns-3 objects
199 that have a TypeId and related introspection information """
201 if type_name not in self.allowed_types:
202 msg = "Type %s not supported" % (type_name)
203 self.logger.error(msg)
205 uuid = self.make_uuid()
208 self.logger.debug("FACTORY %s( %s )" % (type_name, str(kwargs)))
211 self.debuger.dump_factory(uuid, type_name, kwargs)
213 factory = self.ns3.ObjectFactory()
214 factory.SetTypeId(type_name)
216 for name, value in kwargs.items():
217 ns3_value = self._attr_from_string_to_ns3_value(type_name, name, value)
218 factory.Set(name, ns3_value)
220 obj = factory.Create()
222 self._objects[uuid] = obj
225 self.logger.debug("RET FACTORY ( uuid %s ) %s = %s( %s )" % (
226 str(uuid), str(obj), type_name, str(kwargs)))
230 def create(self, clazzname, *args):
231 """ This method should be used to construct ns-3 objects that
232 do not have a TypeId (e.g. Values) """
234 if not hasattr(self.ns3, clazzname):
235 msg = "Type %s not supported" % (clazzname)
236 self.logger.error(msg)
238 uuid = self.make_uuid()
241 self.logger.debug("CREATE %s( %s )" % (clazzname, str(args)))
244 self.debuger.dump_create(uuid, clazzname, args)
246 clazz = getattr(self.ns3, clazzname)
248 # arguments starting with 'uuid' identify ns-3 C++
249 # objects and must be replaced by the actual object
250 realargs = self.replace_args(args)
252 obj = clazz(*realargs)
254 self._objects[uuid] = obj
257 self.logger.debug("RET CREATE ( uuid %s ) %s = %s( %s )" % (str(uuid),
258 str(obj), clazzname, str(args)))
262 def invoke(self, uuid, operation, *args, **kwargs):
264 self.logger.debug("INVOKE %s -> %s( %s, %s ) " % (
265 uuid, operation, str(args), str(kwargs)))
271 if operation == "isRunning":
272 result = self.is_running
274 elif operation == "isStarted":
275 result = self.is_started
277 elif operation == "isFinished":
278 result = self.is_finished
280 elif operation == "isAppRunning":
281 result = self._is_app_running(uuid)
283 elif operation == "isAppStarted":
284 result = self._is_app_started(uuid)
286 elif operation == "recvFD":
287 ### passFD operation binds to a different random socket
288 ### en every execution, so the socket name that could be
289 ### dumped to the debug script using dump_invoke is
290 ### not be valid accross debug executions.
291 result = self._recv_fd(uuid, *args, **kwargs)
293 elif operation == "addStaticRoute":
294 result = self._add_static_route(uuid, *args)
296 ### DUMP - result is static, so will be dumped as plain text
297 self.debuger.dump_invoke(result, uuid, operation, args, kwargs)
299 elif operation == "retrieveObject":
300 result = self._retrieve_object(uuid, *args, **kwargs)
302 ### DUMP - result is static, so will be dumped as plain text
303 self.debuger.dump_invoke(result, uuid, operation, args, kwargs)
306 newuuid = self.make_uuid()
308 ### DUMP - result is a uuid that encoded an dynamically generated
310 self.debuger.dump_invoke(newuuid, uuid, operation, args, kwargs)
312 if uuid.startswith(SINGLETON):
313 obj = self._singleton(uuid)
315 obj = self.get_object(uuid)
317 method = getattr(obj, operation)
319 # arguments starting with 'uuid' identify ns-3 C++
320 # objects and must be replaced by the actual object
321 realargs = self.replace_args(args)
322 realkwargs = self.replace_kwargs(kwargs)
324 result = method(*realargs, **realkwargs)
326 # If the result is an object (not a base value),
327 # then keep track of the object a return the object
328 # reference (newuuid)
329 # xxx the two instances of `int` are from 2to3
330 if not (result is None or type(result) in [
331 bool, float, int, str, int]):
332 self._objects[newuuid] = result
336 self.logger.debug("RET INVOKE %s%s = %s -> %s(%s, %s) " % (
337 "(uuid %s) " % str(newuuid) if newuuid else "", str(result), uuid,
338 operation, str(args), str(kwargs)))
343 def _set_attr(self, obj, name, ns3_value):
344 obj.SetAttribute(name, ns3_value)
346 def set(self, uuid, name, value):
348 self.logger.debug("SET %s %s %s" % (uuid, name, str(value)))
351 self.debuger.dump_set(uuid, name, value)
353 obj = self.get_object(uuid)
354 type_name = obj.GetInstanceTypeId().GetName()
355 ns3_value = self._attr_from_string_to_ns3_value(type_name, name, value)
357 # If the Simulation thread is not running,
358 # then there will be no thread-safety problems
359 # in changing the value of an attribute directly.
360 # However, if the simulation is running we need
361 # to set the value by scheduling an event, else
362 # we risk to corrupt the state of the
365 event_executed = [False]
368 # schedule the event in the Simulator
369 self._schedule_event(self._condition, event_executed,
370 self._set_attr, obj, name, ns3_value)
372 if not event_executed[0]:
373 self._set_attr(obj, name, ns3_value)
376 self.logger.debug("RET SET %s = %s -> set(%s, %s)" % (str(value), uuid, name,
381 def _get_attr(self, obj, name, ns3_value):
382 obj.GetAttribute(name, ns3_value)
384 def get(self, uuid, name):
386 self.logger.debug("GET %s %s" % (uuid, name))
389 self.debuger.dump_get(uuid, name)
391 obj = self.get_object(uuid)
392 type_name = obj.GetInstanceTypeId().GetName()
393 ns3_value = self._create_attr_ns3_value(type_name, name)
395 event_executed = [False]
398 # schedule the event in the Simulator
399 self._schedule_event(self._condition, event_executed,
400 self._get_attr, obj, name, ns3_value)
402 if not event_executed[0]:
403 self._get_attr(obj, name, ns3_value)
405 result = self._attr_from_ns3_value_to_string(type_name, name, ns3_value)
408 self.logger.debug("RET GET %s = %s -> get(%s)" % (str(result), uuid, name))
414 self.debuger.dump_start()
416 # Launch the simulator thread and Start the
417 # simulator in that thread
418 self._condition = threading.Condition()
419 self._simulator_thread = threading.Thread(
420 target = self._simulator_run,
421 args = [self._condition])
422 self._simulator_thread.setDaemon(True)
423 self._simulator_thread.start()
426 self.logger.debug("START")
428 def stop(self, time = None):
430 self.debuger.dump_stop(time=time)
433 self.ns3.Simulator.Stop()
435 self.ns3.Simulator.Stop(self.ns3.Time(time))
438 self.logger.debug("STOP time=%s" % str(time))
442 self.debuger.dump_shutdown()
444 while not self.ns3.Simulator.IsFinished():
445 #self.logger.debug("Waiting for simulation to finish")
448 if self._simulator_thread:
449 self._simulator_thread.join()
451 self.ns3.Simulator.Destroy()
453 # Remove all references to ns-3 objects
454 self._objects.clear()
460 self.logger.debug("SHUTDOWN")
462 def _simulator_run(self, condition):
464 self.ns3.Simulator.Run()
465 # Signal condition to indicate simulation ended and
466 # notify waiting threads
468 condition.notifyAll()
471 def _schedule_event(self, condition, event_executed, func, *args):
472 """ Schedules event on running simulation, and wait until
475 def execute_event(contextId, condition, event_executed, func, *args):
478 event_executed[0] = True
480 # notify condition indicating event was executed
482 condition.notifyAll()
485 # contextId is defined as general context
486 contextId = int(0xffffffff)
488 # delay 0 means that the event is expected to execute inmediately
489 delay = self.ns3.Seconds(0)
491 # Mark event as not executed
492 event_executed[0] = False
496 self.ns3.Simulator.ScheduleWithContext(contextId, delay, execute_event,
497 condition, event_executed, func, *args)
498 if not self.ns3.Simulator.IsFinished():
503 def _create_attr_ns3_value(self, type_name, name):
504 TypeId = self.ns3.TypeId()
505 tid = TypeId.LookupByName(type_name)
506 info = TypeId.AttributeInformation()
507 if not tid.LookupAttributeByName(name, info):
508 msg = "TypeId %s has no attribute %s" % (type_name, name)
509 self.logger.error(msg)
511 checker = info.checker
512 ns3_value = checker.Create()
515 def _attr_from_ns3_value_to_string(self, type_name, name, ns3_value):
516 TypeId = self.ns3.TypeId()
517 tid = TypeId.LookupByName(type_name)
518 info = TypeId.AttributeInformation()
519 if not tid.LookupAttributeByName(name, info):
520 msg = "TypeId %s has no attribute %s" % (type_name, name)
521 self.logger.error(msg)
523 checker = info.checker
524 value = ns3_value.SerializeToString(checker)
526 type_name = checker.GetValueTypeName()
527 if type_name in ["ns3::UintegerValue", "ns3::IntegerValue"]:
529 if type_name == "ns3::DoubleValue":
531 if type_name == "ns3::BooleanValue":
532 return value == "true"
536 def _attr_from_string_to_ns3_value(self, type_name, name, value):
537 TypeId = self.ns3.TypeId()
538 tid = TypeId.LookupByName(type_name)
539 info = TypeId.AttributeInformation()
540 if not tid.LookupAttributeByName(name, info):
541 msg = "TypeId %s has no attribute %s" % (type_name, name)
542 self.logger.error(msg)
544 str_value = str(value)
545 if isinstance(value, bool):
546 str_value = str_value.lower()
548 checker = info.checker
549 ns3_value = checker.Create()
550 ns3_value.DeserializeFromString(str_value, checker)
553 # singletons are identified as "ns3::ClassName"
554 def _singleton(self, ident):
555 if not ident.startswith(SINGLETON):
558 clazzname = ident[ident.find("::")+2:]
559 if not hasattr(self.ns3, clazzname):
560 msg = "Type %s not supported" % (clazzname)
561 self.logger.error(msg)
563 return getattr(self.ns3, clazzname)
565 # replace uuids and singleton references for the real objects
566 def replace_args(self, args):
567 realargs = [self.get_object(arg) if \
568 str(arg).startswith("uuid") else arg for arg in args]
570 realargs = [self._singleton(arg) if \
571 str(arg).startswith(SINGLETON) else arg for arg in realargs]
575 # replace uuids and singleton references for the real objects
576 def replace_kwargs(self, kwargs):
577 realkwargs = dict([(k, self.get_object(v) \
578 if str(v).startswith("uuid") else v) \
579 for k,v in kwargs.items()])
581 realkwargs = dict([(k, self._singleton(v) \
582 if str(v).startswith(SINGLETON) else v )\
583 for k, v in realkwargs.items()])
587 def _is_app_running(self, uuid):
588 now = self.ns3.Simulator.Now()
592 if self.ns3.Simulator.IsFinished():
595 app = self.get_object(uuid)
596 stop_time_value = self.ns3.TimeValue()
597 app.GetAttribute("StopTime", stop_time_value)
598 stop_time = stop_time_value.Get()
600 start_time_value = self.ns3.TimeValue()
601 app.GetAttribute("StartTime", start_time_value)
602 start_time = start_time_value.Get()
604 if now.Compare(start_time) >= 0:
605 if stop_time.IsZero() or now.Compare(stop_time) < 0:
610 def _is_app_started(self, uuid):
611 return self._is_app_running(uuid) or self.is_finished
613 def _add_static_route(self, ipv4_uuid, network, prefix, nexthop):
614 ipv4 = self.get_object(ipv4_uuid)
616 list_routing = ipv4.GetRoutingProtocol()
617 (static_routing, priority) = list_routing.GetRoutingProtocol(0)
619 ifindex = self._find_ifindex(ipv4, nexthop)
623 nexthop = self.ns3.Ipv4Address(nexthop)
625 if network in ["0.0.0.0", "0", None]:
626 # Default route: 0.0.0.0/0
627 static_routing.SetDefaultRoute(nexthop, ifindex)
629 mask = self.ns3.Ipv4Mask("/%s" % prefix)
630 network = self.ns3.Ipv4Address(network)
633 # Host route: x.y.z.w/32
634 static_routing.AddHostRouteTo(network, nexthop, ifindex)
636 # Network route: x.y.z.w/n
637 static_routing.AddNetworkRouteTo(network, mask, nexthop,
641 def _find_ifindex(self, ipv4, nexthop):
644 nexthop = self.ns3.Ipv4Address(nexthop)
646 # For all the interfaces registered with the ipv4 object, find
647 # the one that matches the network of the nexthop
648 nifaces = ipv4.GetNInterfaces()
649 for ifidx in range(nifaces):
650 iface = ipv4.GetInterface(ifidx)
651 naddress = iface.GetNAddresses()
652 for addridx in range(naddress):
653 ifaddr = iface.GetAddress(addridx)
654 ifmask = ifaddr.GetMask()
656 ifindex = ipv4.GetInterfaceForPrefix(nexthop, ifmask)
662 def _retrieve_object(self, uuid, typeid, search = False):
663 obj = self.get_object(uuid)
665 type_id = self.ns3.TypeId()
666 tid = type_id.LookupByName(typeid)
667 nobj = obj.GetObject(tid)
672 for ouuid, oobj in self._objects.items():
677 newuuid = self.make_uuid()
678 self._objects[newuuid] = nobj
682 def _recv_fd(self, uuid):
683 """ Waits on a local address to receive a file descriptor
684 from a local process. The file descriptor is associated
685 to a FdNetDevice to stablish communication between the
686 simulation and what ever process writes on that file descriptor
689 def recvfd(sock, fdnd):
690 (fd, msg) = passfd.recvfd(sock)
691 # Store a reference to the endpoint to keep the socket alive
692 fdnd.SetFileDescriptor(fd)
696 sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
698 address = sock.getsockname()
700 fdnd = self.get_object(uuid)
701 t = threading.Thread(target=recvfd, args=(sock,fdnd))