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