Scripts to automate creation of ns-3 RMs from metadata
[nepi.git] / src / nepi / resources / ns3 / ns3wrapper.py
1 #
2 #    NEPI, a framework to manage network experiments
3 #    Copyright (C) 2013 INRIA
4 #
5 #    This program is free software: you can redistribute it and/or modify
6 #    it under the terms of the GNU General Public License as published by
7 #    the Free Software Foundation, either version 3 of the License, or
8 #    (at your option) any later version.
9 #
10 #    This program is distributed in the hope that it will be useful,
11 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
12 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 #    GNU General Public License for more details.
14 #
15 #    You should have received a copy of the GNU General Public License
16 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
17 #
18 # Author: Alina Quereilhac <alina.quereilhac@inria.fr>
19
20 import logging
21 import os
22 import sys
23 import threading
24 import time
25 import uuid
26
27 # TODO: 
28 #       1. ns-3 classes should be identified as ns3::clazzname?
29
30
31 SINGLETON = "singleton::"
32
33 def load_ns3_module():
34     import ctypes
35     import imp
36     import re
37     import pkgutil
38
39     bindings = os.environ.get("NS3BINDINGS")
40     libdir = os.environ.get("NS3LIBRARIES")
41
42     # Load the ns-3 modules shared libraries
43     if libdir:
44         files = os.listdir(libdir)
45         regex = re.compile("(.*\.so)$")
46         libs = [m.group(1) for filename in files for m in [regex.search(filename)] if m]
47
48         libscp = list(libs)
49         while len(libs) > 0:
50             for lib in libs:
51                 libfile = os.path.join(libdir, lib)
52                 try:
53                     ctypes.CDLL(libfile, ctypes.RTLD_GLOBAL)
54                     libs.remove(lib)
55                 except:
56                     pass
57
58             # if did not load any libraries in the last iteration break
59             # to prevent infinit loop
60             if len(libscp) == len(libs):
61                 raise RuntimeError("Imposible to load shared libraries %s" % str(libs))
62             libscp = list(libs)
63
64     # import the python bindings for the ns-3 modules
65     if bindings:
66         sys.path.append(bindings)
67
68     # create a module to add all ns3 classes
69     ns3mod = imp.new_module("ns3")
70     sys.modules["ns3"] = ns3mod
71
72     # retrieve all ns3 classes and add them to the ns3 module
73     import ns
74
75     for importer, modname, ispkg in pkgutil.iter_modules(ns.__path__):
76         fullmodname = "ns.%s" % modname
77         module = __import__(fullmodname, globals(), locals(), ['*'])
78
79         for sattr in dir(module):
80             if sattr.startswith("_"):
81                 continue
82
83             attr = getattr(module, sattr)
84
85             # netanim.Config and lte.Config singleton overrides ns3::Config
86             if sattr == "Config" and modname in ['netanim', 'lte']:
87                 sattr = "%s.%s" % (modname, sattr)
88
89             setattr(ns3mod, sattr, attr)
90
91     return ns3mod
92
93 class NS3Wrapper(object):
94     def __init__(self, homedir = None):
95         super(NS3Wrapper, self).__init__()
96         # Thread used to run the simulation
97         self._simulation_thread = None
98         self._condition = None
99
100         self._started = False
101
102         # holds reference to all C++ objects and variables in the simulation
103         self._objects = dict()
104
105         # holds the class identifiers of uuid to be able to retrieve
106         # the corresponding ns3 TypeId to set/get attributes.
107         # This is necessary because the method GetInstanceTypeId is not
108         # exposed through the Python bindings
109         self._tids = dict()
110
111         # create home dir (where all simulation related files will end up)
112         self._homedir = homedir or os.path.join("/", "tmp", "ns3_wrapper" )
113         
114         home = os.path.normpath(self.homedir)
115         if not os.path.exists(home):
116             os.makedirs(home, 0755)
117
118         # Logging
119         loglevel = os.environ.get("NS3LOGLEVEL", "debug")
120         self._logger = logging.getLogger("ns3wrapper")
121         self._logger.setLevel(getattr(logging, loglevel.upper()))
122         
123         hdlr = logging.FileHandler(os.path.join(self.homedir, "ns3wrapper.log"))
124         formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
125         hdlr.setFormatter(formatter)
126         
127         self._logger.addHandler(hdlr) 
128
129         # Python module to refernce all ns-3 classes and types
130         self._ns3 = None
131         
132     @property
133     def ns3(self):
134         if not self._ns3:
135             self._ns3 = load_ns3_module()
136
137         return self._ns3
138
139     @property
140     def homedir(self):
141         return self._homedir
142
143     @property
144     def logger(self):
145         return self._logger
146
147     @property
148     def is_running(self):
149         return self._started and self._ns3 and not self.ns3.Simulator.IsFinished()
150
151     def make_uuid(self):
152         return "uuid%s" % uuid.uuid4()
153
154     def get_object(self, uuid):
155         return self._objects.get(uuid)
156
157     def get_typeid(self, uuid):
158         return self._tids.get(uuid)
159
160     def create(self, clazzname, *args):
161         if not hasattr(self.ns3, clazzname):
162             msg = "Type %s not supported" % (clazzname) 
163             self.logger.error(msg)
164      
165         clazz = getattr(self.ns3, clazzname)
166  
167         # arguments starting with 'uuid' identify ns-3 C++
168         # objects and must be replaced by the actual object
169         realargs = self.replace_args(args)
170        
171         obj = clazz(*realargs)
172         
173         uuid = self.make_uuid()
174         self._objects[uuid] = obj
175
176         #typeid = clazz.GetInstanceTypeId().GetName()
177         typeid = "ns3::%s" % clazzname
178         self._tids[uuid] = typeid
179
180         return uuid
181
182     def invoke(self, uuid, operation, *args):
183         if uuid.startswith(SINGLETON):
184             obj = self._singleton(uuid)
185         else:
186             obj = self.get_object(uuid)
187     
188         method = getattr(obj, operation)
189
190         # arguments starting with 'uuid' identify ns-3 C++
191         # objects and must be replaced by the actual object
192         realargs = self.replace_args(args)
193
194         result = method(*realargs)
195
196         if not result:
197             return None
198         
199         newuuid = self.make_uuid()
200         self._objects[newuuid] = result
201
202         return newuuid
203
204     def set(self, uuid, name, value):
205         obj = self.get_object(uuid)
206         ns3_value = self._to_ns3_value(uuid, name, value)
207
208         def set_attr(obj, name, ns3_value):
209             obj.SetAttribute(name, ns3_value)
210
211         # If the Simulation thread is not running,
212         # then there will be no thread-safety problems
213         # in changing the value of an attribute directly.
214         # However, if the simulation is running we need
215         # to set the value by scheduling an event, else
216         # we risk to corrupt the state of the
217         # simulation.
218         if self.is_running:
219             # schedule the event in the Simulator
220             self._schedule_event(self._condition, set_attr, obj,
221                     name, ns3_value)
222         else:
223             set_attr(obj, name, ns3_value)
224
225         return value
226
227     def get(self, uuid, name):
228         obj = self.get_object(uuid)
229         ns3_value = self._create_ns3_value(uuid, name)
230
231         def get_attr(obj, name, ns3_value):
232             obj.GetAttribute(name, ns3_value)
233
234         if self.is_running:
235             # schedule the event in the Simulator
236             self._schedule_event(self._condition, get_attr, obj,
237                     name, ns3_value)
238         else:
239             get_attr(obj, name, ns3_value)
240
241         return self._from_ns3_value(uuid, name, ns3_value)
242
243     def start(self):
244         # Launch the simulator thread and Start the
245         # simulator in that thread
246         self._condition = threading.Condition()
247         self._simulator_thread = threading.Thread(
248                 target = self._simulator_run,
249                 args = [self._condition])
250         self._simulator_thread.setDaemon(True)
251         self._simulator_thread.start()
252         self._started = True
253
254     def stop(self, time = None):
255         if not self.ns3:
256             return
257
258         if time is None:
259             self.ns3.Simulator.Stop()
260         else:
261             self.ns3.Simulator.Stop(self.ns3.Time(time))
262
263     def shutdown(self):
264         if self.ns3:
265             while not self.ns3.Simulator.IsFinished():
266                 #self.logger.debug("Waiting for simulation to finish")
267                 time.sleep(0.5)
268             
269             # TODO!!!! SHOULD WAIT UNTIL THE THREAD FINISHES
270             if self._simulator_thread:
271                 self._simulator_thread.join()
272             
273             self.ns3.Simulator.Destroy()
274         
275         # Remove all references to ns-3 objects
276         self._objects.clear()
277         
278         self._ns3 = None
279         sys.stdout.flush()
280         sys.stderr.flush()
281
282     def _simulator_run(self, condition):
283         # Run simulation
284         self.ns3.Simulator.Run()
285         # Signal condition to indicate simulation ended and
286         # notify waiting threads
287         condition.acquire()
288         condition.notifyAll()
289         condition.release()
290
291     def _schedule_event(self, condition, func, *args):
292         """ Schedules event on running simulation, and wait until
293             event is executed"""
294
295         def execute_event(contextId, condition, has_event_occurred, func, *args):
296             try:
297                 func(*args)
298             finally:
299                 # flag event occured
300                 has_event_occurred[0] = True
301                 # notify condition indicating attribute was set
302                 condition.acquire()
303                 condition.notifyAll()
304                 condition.release()
305
306         # contextId is defined as general context
307         contextId = long(0xffffffff)
308
309         # delay 0 means that the event is expected to execute inmediately
310         delay = self.ns3.Seconds(0)
311
312         # flag to indicate that the event occured
313         # because bool is an inmutable object in python, in order to create a
314         # bool flag, a list is used as wrapper
315         has_event_occurred = [False]
316         condition.acquire()
317         try:
318             if not self.ns3.Simulator.IsFinished():
319                 self.ns3.Simulator.ScheduleWithContext(contextId, delay, execute_event,
320                      condition, has_event_occurred, func, *args)
321                 while not has_event_occurred[0] and not self.ns3.Simulator.IsFinished():
322                     condition.wait()
323         finally:
324             condition.release()
325
326     def _create_ns3_value(self, uuid, name):
327         typeid = self.get_typeid(uuid)
328         TypeId = self.ns3.TypeId()
329         tid = TypeId.LookupByName(typeid)
330         info = TypeId.AttributeInformation()
331         if not tid.LookupAttributeByName(name, info):
332             msg = "TypeId %s has no attribute %s" % (typeid, name) 
333             self.logger.error(msg)
334
335         checker = info.checker
336         ns3_value = checker.Create() 
337         return ns3_value
338
339     def _from_ns3_value(self, uuid, name, ns3_value):
340         typeid = self.get_typeid(uuid)
341         TypeId = self.ns3.TypeId()
342         tid = TypeId.LookupByName(typeid)
343         info = TypeId.AttributeInformation()
344         if not tid.LookupAttributeByName(name, info):
345             msg = "TypeId %s has no attribute %s" % (typeid, name) 
346             self.logger.error(msg)
347
348         checker = info.checker
349         value = ns3_value.SerializeToString(checker)
350
351         type_name = checker.GetValueTypeName()
352         if type_name in ["ns3::UintegerValue", "ns3::IntegerValue"]:
353             return int(value)
354         if type_name == "ns3::DoubleValue":
355             return float(value)
356         if type_name == "ns3::BooleanValue":
357             return value == "true"
358
359         return value
360
361     def _to_ns3_value(self, uuid, name, value):
362         typeid = self.get_typeid(uuid)
363         TypeId = self.ns3.TypeId()
364         tid = TypeId.LookupByName(typeid)
365         info = TypeId.AttributeInformation()
366         if not tid.LookupAttributeByName(name, info):
367             msg = "TypeId %s has no attribute %s" % (typeid, name) 
368             self.logger.error(msg)
369
370         str_value = str(value)
371         if isinstance(value, bool):
372             str_value = str_value.lower()
373
374         checker = info.checker
375         ns3_value = checker.Create()
376         ns3_value.DeserializeFromString(str_value, checker)
377         return ns3_value
378
379     # singletons are identified as "ns3::ClassName"
380     def _singleton(self, ident):
381         if not ident.startswith(SINGLETON):
382             return None
383
384         clazzname = ident[ident.find("::")+2:]
385         if not hasattr(self.ns3, clazzname):
386             msg = "Type %s not supported" % (clazzname)
387             self.logger.error(msg)
388
389         return getattr(self.ns3, clazzname)
390
391     # replace uuids and singleton references for the real objects
392     def replace_args(self, args):
393         realargs = [self.get_object(arg) if \
394                 str(arg).startswith("uuid") else arg for arg in args]
395  
396         realargs = [self._singleton(arg) if \
397                 str(arg).startswith(SINGLETON) else arg for arg in realargs]
398
399         return realargs
400