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