c2d65d3743d484e038711bed232e825ceda54863
[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, ResourceFactory, reschedule_delay
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 = "LinuxNetNSEmulation"
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(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_release(self):
178         self.info("Releasing resource")
179
180         tear_down = self.get("tearDown")
181         if tear_down:
182             self.node.execute(tear_down)
183
184         self.do_stop()
185         self._client.shutdown()
186         LinuxApplication.do_stop(self)
187         
188         super(LinuxApplication, self).do_release()
189
190     @property
191     def _start_command(self):
192         command = [] 
193
194         #command.append("sudo")
195         command.append("PYTHONPATH=$PYTHONPATH:${SRC}/netnswrapper/")
196         command.append("python ${SRC}/netnswrapper/netnsserver.py -S %s" % \
197                 os.path.basename(self.remote_socket) )
198
199         if self.get("enableDump"):
200             command.append("-D")
201
202         if self.get("verbose"):
203             command.append("-v")
204
205         command = " ".join(command)
206         return command
207
208     @property
209     def _dependencies(self):
210         if self.node.use_rpm:
211             return ( " python python-devel mercurial unzip bridge-utils iproute")
212         elif self.node.use_deb:
213             return ( " python python-dev mercurial unzip bridge-utils iproute")
214         return ""
215
216     @property
217     def netns_repo(self):
218         return "http://nepi.inria.fr/code/netns"
219
220     @property
221     def netns_version(self):
222         version = self.get("version")
223         return version or "dev"
224
225     @property
226     def python_unshare_repo(self):
227         return "http://nepi.inria.fr/code/python-unshare"
228
229     @property
230     def python_unshare_version(self):
231         return "dev"
232
233     @property
234     def python_passfd_repo(self):
235         return "http://nepi.inria.fr/code/python-passfd"
236
237     @property
238     def python_passfd_version(self):
239         return "dev"
240
241     @property
242     def netns_src(self):
243         location = "${SRC}/netns/%(version)s" \
244                     % {
245                         "version": self.netns_version,
246                       }
247
248         return location
249
250     @property
251     def python_unshare_src(self):
252         location = "${SRC}/python_unshare/%(version)s" \
253                     % {
254                         "version": self.python_unshare_version,
255                       }
256
257         return location
258
259     @property
260     def python_passfd_src(self):
261         location = "${SRC}/python_passfd/%(version)s" \
262                     % {
263                         "version": self.python_passfd_version,
264                       }
265
266         return location
267
268     def clone_command(self, name, repo, src):
269         clone_cmd = (
270                 # Test if alredy cloned
271                 " ( "
272                 "  ( "
273                 "    ( test -d %(src)s ) "
274                 "   && echo '%(name)s binaries found, nothing to do'"
275                 "  ) "
276                 " ) "
277                 "  || " 
278                 # clone source code
279                 " ( "
280                 "   mkdir -p %(src)s && "
281                 "   hg clone %(repo)s %(src)s"
282                 " ) "
283              ) % {
284                     "repo": repo,
285                     "src": src,
286                     "name": name,
287                  }
288
289         return clone_cmd
290
291     @property
292     def _build(self):
293         netns_clone = self.clone_command("netns", self.netns_repo, 
294                 self.netns_src)
295         python_unshare_clone = self.clone_command("python_unshare", 
296                 self.python_unshare_repo, self.python_unshare_src)
297         python_passfd_clone = self.clone_command("python_passfd", 
298                 self.python_passfd_repo, self.python_passfd_src)
299
300         build_cmd = (
301                 # Netns installation
302                 "( %(netns_clone)s )"
303                 "  && "
304                 "( %(python_unshare_clone)s )"
305                 "  && "
306                 "( %(python_passfd_clone)s )"
307              ) % { 
308                     "netns_clone": netns_clone,
309                     "python_unshare_clone": python_unshare_clone,  
310                     "python_passfd_clone": python_passfd_clone,  
311                  }
312
313         return build_cmd
314
315     @property
316     def _environment(self):
317         env = []
318         env.append("PYTHONPATH=$PYTHONPAH:%(netns_src)s/src/:%(python_unshare_src)s/src:%(python_passfd_src)s/src}" % { 
319                     "netns_src": self.netns_src,
320                     "python_unshare_src": self.python_unshare_src,
321                     "python_passfd_src": self.python_passfd_src,
322                  })
323
324         return " ".join(env) 
325
326     def replace_paths(self, command):
327         """
328         Replace all special path tags with shell-escaped actual paths.
329         """
330         return ( command
331             .replace("${USR}", self.node.usr_dir)
332             .replace("${LIB}", self.node.lib_dir)
333             .replace("${BIN}", self.node.bin_dir)
334             .replace("${SRC}", self.node.src_dir)
335             .replace("${SHARE}", self.node.share_dir)
336             .replace("${EXP}", self.node.exp_dir)
337             .replace("${EXP_HOME}", self.node.exp_home)
338             .replace("${APP_HOME}", self.app_home)
339             .replace("${RUN_HOME}", self.run_home)
340             .replace("${NODE_HOME}", self.node.node_home)
341             .replace("${HOME}", self.node.home_dir)
342             )
343
344     def valid_connection(self, guid):
345         # TODO: Validate!
346         return True
347
348     def wait_remote_socket(self):
349         """ Waits until the remote socket is created
350         """
351         command = " [ -e %s ] && echo 'DONE' " % self.remote_socket
352
353         for i in xrange(200):
354             (out, err), proc = self.node.execute(command, retry = 1, 
355                     with_lock = True)
356
357             if out.find("DONE") > -1:
358                 break
359         else:
360             raise RuntimeError("Remote socket not found at %s" % \
361                     self.remote_socket)
362     
363