Serious refactoring of TUN/TAP and tunnel code. Linux udp/gre tunnels not yet functional
[nepi.git] / src / nepi / resources / linux / netns / netnsemulation.py
1 #
2 #    NEPI, a framework to manage network experiments
3 #    Copyright (C) 2014 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 from nepi.execution.attribute import Attribute, Flags, Types
21 from nepi.execution.trace import Trace, TraceAttr
22 from nepi.execution.resource import ResourceManager, clsinit_copy, \
23         ResourceState
24 from nepi.resources.linux.application import LinuxApplication
25 from nepi.util.timefuncs import tnow, tdiffsec
26 from nepi.resources.netns.netnsemulation import NetNSEmulation
27 from nepi.resources.linux.netns.netnsclient import LinuxNetNSClient
28
29 import os
30 import time
31 import threading
32
33 @clsinit_copy
34 class LinuxNetNSEmulation(LinuxApplication, NetNSEmulation):
35     _rtype = "linux::netns::Emulation"
36
37     @classmethod
38     def _register_attributes(cls):
39         verbose = Attribute("verbose",
40             "True to output debugging info for the client-server communication",
41             type = Types.Bool,
42             flags = Flags.Design)
43
44         enable_dump = Attribute("enableDump",
45             "Enable dumping the remote executed commands to a script "
46             "in order to later reproduce and debug the experiment",
47             type = Types.Bool,
48             default = False,
49             flags = Flags.Design)
50
51         version = Attribute("version",
52             "Version of netns to install from nsam repo",
53             default = "netns-dev", 
54             flags = Flags.Design)
55
56         cls._register_attribute(enable_dump)
57         cls._register_attribute(verbose)
58         cls._register_attribute(version)
59
60     def __init__(self, ec, guid):
61         LinuxApplication.__init__(self, ec, guid)
62         NetNSEmulation.__init__(self)
63
64         self._client = None
65         self._home = "netns-emu-%s" % self.guid
66         self._socket_name = "netns-%s.sock" % os.urandom(4).encode('hex')
67
68     @property
69     def socket_name(self):
70         return self._socket_name
71
72     @property
73     def remote_socket(self):
74         return os.path.join(self.run_home, self.socket_name)
75
76     def upload_sources(self):
77         self.node.mkdir(os.path.join(self.node.src_dir, "netnswrapper"))
78
79         # upload wrapper python script
80         wrapper = os.path.join(os.path.dirname(__file__), "..", "..", "netns", 
81                 "netnswrapper.py")
82
83         self.node.upload(wrapper,
84                 os.path.join(self.node.src_dir, "netnswrapper", "netnswrapper.py"),
85                 overwrite = False)
86
87         # upload wrapper debug python script
88         wrapper_debug = os.path.join(os.path.dirname(__file__), "..", "..", "netns", 
89                 "netnswrapper_debug.py")
90
91         self.node.upload(wrapper_debug,
92                 os.path.join(self.node.src_dir, "netnswrapper", "netnswrapper_debug.py"),
93                 overwrite = False)
94
95         # upload server python script
96         server = os.path.join(os.path.dirname(__file__), "..", "..", "netns",
97                 "netnsserver.py")
98
99         self.node.upload(server,
100                 os.path.join(self.node.src_dir, "netnswrapper", "netnsserver.py"),
101                 overwrite = False)
102
103         # Upload user defined sources
104         self.node.mkdir(os.path.join(self.node.src_dir, "netns"))
105         src_dir = os.path.join(self.node.src_dir, "netns")
106
107         super(LinuxNetNSEmulation, self).upload_sources(src_dir = src_dir)
108     
109     def upload_extra_sources(self, sources = None, src_dir = None):
110         return super(LinuxNetNSEmulation, self).upload_sources(
111                 sources = sources, 
112                 src_dir = src_dir)
113
114     def upload_start_command(self):
115         command = self.get("command")
116         env = self.get("env")
117
118         # We want to make sure the emulator is running
119         # before the experiment starts.
120         # Run the command as a bash script in background,
121         # in the host ( but wait until the command has
122         # finished to continue )
123         env = self.replace_paths(env)
124         command = self.replace_paths(command)
125
126         shfile = os.path.join(self.app_home, "start.sh")
127         self.node.upload_command(command, 
128                     shfile = shfile,
129                     env = env,
130                     overwrite = True)
131
132         # Run the wrapper 
133         self._run_in_background()
134
135         # Wait until the remote socket is created
136         self.wait_remote_socket()
137
138     def do_deploy(self):
139         if not self.node or self.node.state < ResourceState.READY:
140             self.debug("---- RESCHEDULING DEPLOY ---- node state %s " % self.node.state )
141             
142             # ccnd needs to wait until node is deployed and running
143             self.ec.schedule(self.reschedule_delay, self.deploy)
144         else:
145             if not self.get("command"):
146                 self.set("command", self._start_command)
147             
148             if not self.get("depends"):
149                 self.set("depends", self._dependencies)
150
151             if self.get("sources"):
152                 sources = self.get("sources")
153                 source = sources.split(" ")[0]
154                 basename = os.path.basename(source)
155                 version = ( basename.strip().replace(".tar.gz", "")
156                     .replace(".tar","")
157                     .replace(".gz","")
158                     .replace(".zip","") )
159
160                 self.set("version", version)
161                 self.set("sources", source)
162
163             if not self.get("build"):
164                 self.set("build", self._build)
165
166             if not self.get("env"):
167                 self.set("env", self._environment)
168
169             self.do_discover()
170             self.do_provision()
171
172             # Create client
173             self._client = LinuxNetNSClient(self)
174
175             self.set_ready()
176
177     def do_start(self):
178         """ Starts  execution execution
179
180         """
181         self.info("Starting")
182
183         if self.state == ResourceState.READY:
184             self.set_started()
185         else:
186             msg = " Failed to execute command '%s'" % command
187             self.error(msg, out, err)
188             raise RuntimeError, msg
189
190     def do_stop(self):
191         """ Stops simulation execution
192
193         """
194         if self.state == ResourceState.STARTED:
195             self.set_stopped()
196
197     def do_release(self):
198         self.info("Releasing resource")
199
200         tear_down = self.get("tearDown")
201         if tear_down:
202             self.node.execute(tear_down)
203
204         self.do_stop()
205         self._client.shutdown()
206         LinuxApplication.do_stop(self)
207         
208         super(LinuxApplication, self).do_release()
209
210     @property
211     def _start_command(self):
212         command = [] 
213
214         #command.append("sudo")
215         command.append("PYTHONPATH=$PYTHONPATH:${SRC}/netnswrapper/")
216         command.append("python ${SRC}/netnswrapper/netnsserver.py -S %s" % \
217                 os.path.basename(self.remote_socket) )
218
219         if self.get("enableDump"):
220             command.append("-D")
221
222         if self.get("verbose"):
223             command.append("-v")
224
225         command = " ".join(command)
226         return command
227
228     @property
229     def _dependencies(self):
230         if self.node.use_rpm:
231             return (" python python-devel mercurial unzip bridge-utils iproute")
232         elif self.node.use_deb:
233             return (" python python-dev mercurial unzip bridge-utils iproute")
234         return ""
235
236     @property
237     def netns_repo(self):
238         return "http://nepi.inria.fr/code/netns"
239
240     @property
241     def netns_version(self):
242         version = self.get("version")
243         return version or "dev"
244
245     @property
246     def python_unshare_repo(self):
247         return "http://nepi.inria.fr/code/python-unshare"
248
249     @property
250     def python_unshare_version(self):
251         return "dev"
252
253     @property
254     def python_passfd_repo(self):
255         return "http://nepi.inria.fr/code/python-passfd"
256
257     @property
258     def python_passfd_version(self):
259         return "dev"
260
261     @property
262     def netns_src(self):
263         location = "${SRC}/netns/%(version)s" \
264                     % {
265                         "version": self.netns_version,
266                       }
267
268         return location
269
270     @property
271     def python_unshare_src(self):
272         location = "${SRC}/python_unshare/%(version)s" \
273                     % {
274                         "version": self.python_unshare_version,
275                       }
276
277         return location
278
279     @property
280     def python_passfd_src(self):
281         location = "${SRC}/python_passfd/%(version)s" \
282                     % {
283                         "version": self.python_passfd_version,
284                       }
285
286         return location
287
288     def clone_command(self, name, repo, src):
289         clone_cmd = (
290                 # Test if alredy cloned
291                 " ( "
292                 "  ( "
293                 "    ( test -d %(src)s ) "
294                 "   && echo '%(name)s binaries found, nothing to do'"
295                 "  ) "
296                 " ) "
297                 "  || " 
298                 # clone source code
299                 " ( "
300                 "   mkdir -p %(src)s && "
301                 "   hg clone %(repo)s %(src)s"
302                 " ) "
303              ) % {
304                     "repo": repo,
305                     "src": src,
306                     "name": name,
307                  }
308
309         return clone_cmd
310
311     @property
312     def _build(self):
313         netns_clone = self.clone_command("netns", self.netns_repo, 
314                 self.netns_src)
315         python_unshare_clone = self.clone_command("python_unshare", 
316                 self.python_unshare_repo, self.python_unshare_src)
317         python_passfd_clone = self.clone_command("python_passfd", 
318                 self.python_passfd_repo, self.python_passfd_src)
319
320         build_cmd = (
321                 # Netns installation
322                 "( %(netns_clone)s )"
323                 "  && "
324                 "( %(python_unshare_clone)s )"
325                 "  && "
326                 "( %(python_passfd_clone)s )"
327              ) % { 
328                     "netns_clone": netns_clone,
329                     "python_unshare_clone": python_unshare_clone,  
330                     "python_passfd_clone": python_passfd_clone,  
331                  }
332
333         return build_cmd
334
335     @property
336     def _environment(self):
337         env = []
338         env.append("PYTHONPATH=$PYTHONPAH:%(netns_src)s/src/:%(python_unshare_src)s/src:%(python_passfd_src)s/src}" % { 
339                     "netns_src": self.netns_src,
340                     "python_unshare_src": self.python_unshare_src,
341                     "python_passfd_src": self.python_passfd_src,
342                  })
343
344         return " ".join(env) 
345
346     def replace_paths(self, command):
347         """
348         Replace all special path tags with shell-escaped actual paths.
349         """
350         return ( command
351             .replace("${USR}", self.node.usr_dir)
352             .replace("${LIB}", self.node.lib_dir)
353             .replace("${BIN}", self.node.bin_dir)
354             .replace("${SRC}", self.node.src_dir)
355             .replace("${SHARE}", self.node.share_dir)
356             .replace("${EXP}", self.node.exp_dir)
357             .replace("${EXP_HOME}", self.node.exp_home)
358             .replace("${APP_HOME}", self.app_home)
359             .replace("${RUN_HOME}", self.run_home)
360             .replace("${NODE_HOME}", self.node.node_home)
361             .replace("${HOME}", self.node.home_dir)
362             )
363
364     def valid_connection(self, guid):
365         # TODO: Validate!
366         return True
367
368     def wait_remote_socket(self):
369         """ Waits until the remote socket is created
370         """
371         command = " [ -e %s ] && echo 'DONE' " % self.remote_socket
372
373         for i in xrange(200):
374             (out, err), proc = self.node.execute(command, retry = 1, 
375                     with_lock = True)
376
377             if out.find("DONE") > -1:
378                 break
379         else:
380             raise RuntimeError("Remote socket not found at %s" % \
381                     self.remote_socket)
382     
383