Ns-3/linux tunnel experiments
[nepi.git] / src / nepi / resources / linux / ns3 / fdtunnel.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 from nepi.execution.attribute import Attribute, Flags, Types
21 from nepi.execution.resource import clsinit_copy, ResourceState
22 from nepi.resources.linux.udptunnel import LinuxUdpTunnel
23 from nepi.util.sshfuncs import ProcStatus
24 from nepi.util.timefuncs import tnow, tdiffsec
25
26 import base64
27 import os
28 import socket
29 import time
30
31 @clsinit_copy
32 class LinuxNs3FdUdpTunnel(LinuxUdpTunnel):
33     _rtype = "linux::ns3::FdUdpTunnel"
34     _help = "Constructs a tunnel between two Linux FileDescriptorNetdevices using a UDP connection "
35     _platform = "linux::ns3"
36
37     @classmethod
38     def _register_attributes(cls):
39         cipher = Attribute("cipher",
40                "Cipher to encript communication. "
41                 "One of PLAIN, AES, Blowfish, DES, DES3. ",
42                 default = None,
43                 allowed = ["PLAIN", "AES", "Blowfish", "DES", "DES3"],
44                 type = Types.Enumerate, 
45                 flags = Flags.Design)
46
47         cipher_key = Attribute("cipherKey",
48                 "Specify a symmetric encryption key with which to protect "
49                 "packets across the tunnel. python-crypto must be installed "
50                 "on the system." ,
51                 flags = Flags.Design)
52
53         txqueuelen = Attribute("txQueueLen",
54                 "Specifies the interface's transmission queue length. "
55                 "Defaults to 1000. ", 
56                 type = Types.Integer, 
57                 flags = Flags.Design)
58
59         bwlimit = Attribute("bwLimit",
60                 "Specifies the interface's emulated bandwidth in bytes "
61                 "per second.",
62                 type = Types.Integer, 
63                 flags = Flags.Design)
64
65         cls._register_attribute(cipher)
66         cls._register_attribute(cipher_key)
67         cls._register_attribute(txqueuelen)
68         cls._register_attribute(bwlimit)
69
70     def __init__(self, ec, guid):
71         super(LinuxUdpTunnel, self).__init__(ec, guid)
72         self._home = "fd-udp-tunnel-%s" % self.guid
73         self._pids = dict()
74         self._fd1 = None
75         self._fd1node = None
76         self._fd2 = None
77         self._fd2node = None
78
79     def log_message(self, msg):
80         self.get_endpoints()
81         return " guid %d - fd-udptunnel %s - %s - %s " % (self.guid, 
82                 self.node1.get("hostname"), 
83                 self.node2.get("hostname"), 
84                 msg)
85
86     def get_endpoints(self):
87         """ Returns the list of RM that are endpoints to the tunnel 
88         """
89         if not self._fd2 or not self._fd1:
90             from nepi.resources.ns3.ns3fdnetdevice import NS3BaseFdNetDevice
91             devices = self.get_connected(NS3BaseFdNetDevice.get_rtype())
92             if not devices or len(devices) != 2: 
93                 msg = "linux::ns3::TunTapFdLink must be connected to exactly one FdNetDevice"
94                 self.error(msg)
95                 raise RuntimeError, msg
96
97             self._fd1 = devices[0]
98             self._fd2 = devices[1]
99         
100             simu = self._fd1.simulation
101             from nepi.resources.linux.node import LinuxNode
102             nodes = simu.get_connected(LinuxNode.get_rtype())
103             self._fd1node = nodes[0]
104      
105             simu = self._fd2.simulation
106             from nepi.resources.linux.node import LinuxNode
107             nodes = simu.get_connected(LinuxNode.get_rtype())
108             self._fd2node = nodes[0]
109
110             if self._fd1node.get("hostname") == \
111                     self._fd2node.get("hostname"):
112                 msg = "linux::ns3::FdUdpTunnel requires endpoints on different hosts"
113                 self.error(msg)
114                 raise RuntimeError, msg
115
116         return [self._fd1, self._fd2]
117
118     @property
119     def endpoint1(self):
120         return self._fd1
121
122     @property
123     def endpoint2(self):
124         return self._fd2
125
126     @property
127     def node1(self):
128         return self._fd1node
129
130     @property
131     def node2(self):
132         return self._fd2node
133
134     def endpoint_node(self, endpoint):
135         node = None
136         if endpoint == self.endpoint1:
137             node = self.node1
138         else:
139             node = self.node2
140
141         return node
142  
143     def app_home(self, endpoint):
144         node = self.endpoint_node(endpoint)
145         return os.path.join(node.exp_home, self._home)
146
147     def run_home(self, endpoint):
148         return os.path.join(self.app_home(endpoint), self.ec.run_id)
149
150     def upload_sources(self, endpoint):
151         scripts = []
152
153         # vif-passfd python script
154         linux_passfd = os.path.join(os.path.dirname(__file__),
155                 "..",
156                 "scripts",
157                 "linux-ns3-fd-udp-connect.py")
158
159         scripts.append(linux_passfd)
160         
161         # Upload scripts
162         scripts = ";".join(scripts)
163
164         node = self.endpoint_node(endpoint)
165         node.upload(scripts,
166                 os.path.join(node.src_dir),
167                 overwrite = False)
168
169     def endpoint_mkdir(self, endpoint):
170         node = self.endpoint_node(endpoint) 
171         run_home = self.run_home(endpoint)
172         node.mkdir(run_home)
173
174     def initiate_connection(self, endpoint, remote_endpoint):
175         cipher = self.get("cipher")
176         cipher_key = self.get("cipherKey")
177         bwlimit = self.get("bwLimit")
178         txqueuelen = self.get("txQueueLen")
179
180         # Upload the tunnel creating script
181         self.upload_sources(endpoint)
182
183         # Request an address to send the file descriptor to the ns-3 simulation
184         address = endpoint.recv_fd()
185
186         # execute the tunnel creation script
187         node = self.endpoint_node(remote_endpoint) 
188         remote_ip = node.get("ip")
189         port = self.initiate(endpoint, address, remote_ip, cipher, 
190                 cipher_key, bwlimit, txqueuelen)
191
192         return port
193
194     def establish_connection(self, endpoint, remote_endpoint, port):
195         self.establish(remote_endpoint, port)
196
197     def verify_connection(self, endpoint, remote_endpoint):
198         self.verify()
199
200     def terminate_connection(self, endpoint, remote_endpoint):
201         # Nothing to do
202         return
203
204     def check_state_connection(self):
205         # Make sure the process is still running in background
206         # No execution errors occurred. Make sure the background
207         # process with the recorded pid is still running.
208
209         node1 = self.endpoint_node(self.endpoint1) 
210         node2 = self.endpoint_node(self.endpoint2) 
211         run_home1 = self.run_home(self.endpoint1)
212         run_home2 = self.run_home(self.endpoint1)
213         (pid1, ppid1) = self._pids[endpoint1]
214         (pid2, ppid2) = self._pids[endpoint2]
215         
216         status1 = node1.status(pid1, ppid1)
217         status2 = node2.status(pid2, ppid2)
218
219         if status1 == ProcStatus.FINISHED and \
220                 status2 == ProcStatus.FINISHED:
221
222             # check if execution errors occurred
223             (out1, err1), proc1 = node1.check_errors(run_home1)
224             (out2, err2), proc2 = node2.check_errors(run_home2)
225
226             if err1 or err2: 
227                 msg = "Error occurred in tunnel"
228                 self.error(msg, err1, err2)
229                 self.fail()
230             else:
231                 self.set_stopped()
232
233     def wait_local_port(self, endpoint):
234         """ Waits until the local_port file for the endpoint is generated, 
235         and returns the port number 
236         
237         """
238         return self.wait_file(endpoint, "local_port")
239
240     def wait_result(self, endpoint):
241         """ Waits until the return code file for the endpoint is generated 
242         
243         """ 
244         return self.wait_file(endpoint, "ret_file")
245  
246     def wait_file(self, endpoint, filename):
247         """ Waits until file on endpoint is generated """
248         result = None
249         delay = 1.0
250         
251         node = self.endpoint_node(endpoint) 
252         run_home = self.run_home(endpoint)
253
254         for i in xrange(20):
255             (out, err), proc = node.check_output(run_home, filename)
256
257             if out:
258                 result = out.strip()
259                 break
260             else:
261                 time.sleep(delay)
262                 delay = delay * 1.5
263         else:
264             msg = "Couldn't retrieve %s" % filename
265             self.error(msg, out, err)
266             raise RuntimeError, msg
267
268         return result
269
270     def initiate(self, endpoint, address, remote_ip, cipher, cipher_key, 
271             bwlimit, txqueuelen):
272         command = self._initiate_command(endpoint, address, remote_ip, 
273                 cipher, cipher_key, bwlimit, txqueuelen)
274
275         node = self.endpoint_node(endpoint) 
276         run_home = self.run_home(endpoint)
277         app_home = self.app_home(endpoint)
278
279         # upload command to connect.sh script
280         shfile = os.path.join(app_home, "fd-udp-connect.sh")
281         node.upload_command(command,
282                 shfile = shfile,
283                 overwrite = False)
284
285         # invoke connect script
286         cmd = "bash %s" % shfile
287         (out, err), proc = node.run(cmd, run_home) 
288              
289         # check if execution errors occurred
290         msg = "Failed to connect endpoints "
291         
292         if proc.poll():
293             self.error(msg, out, err)
294             raise RuntimeError, msg
295     
296         # Wait for pid file to be generated
297         pid, ppid = node.wait_pid(run_home)
298
299         self._pids[endpoint] = (pid, ppid)
300         
301         # Check for error information on the remote machine
302         (out, err), proc = node.check_errors(run_home)
303         # Out is what was written in the stderr file
304         if err:
305             msg = " Failed to start command '%s' " % command
306             self.error(msg, out, err)
307             raise RuntimeError, msg
308
309         port = self.wait_local_port(endpoint)
310
311         return port
312
313     def _initiate_command(self, endpoint, address, remote_ip, 
314             cipher, cipher_key, bwlimit, txqueuelen):
315         node = self.endpoint_node(endpoint) 
316         run_home = self.run_home(endpoint)
317         app_home = self.app_home(endpoint)
318
319         local_port_file = os.path.join(run_home, "local_port")
320         remote_port_file = os.path.join(run_home,  "remote_port")
321         ret_file = os.path.join(run_home, "ret_file")
322
323         address = base64.b64encode(address)
324         
325         command = [""]
326         # Use pl-vid-udp-connect.py to stablish the tunnel between endpoints
327         command.append("PYTHONPATH=$PYTHONPATH:${SRC}")
328         command.append("python ${SRC}/linux-ns3-fd-udp-connect.py")
329         command.append("-a %s" % address)
330         command.append("-l %s " % local_port_file)
331         command.append("-r %s " % remote_port_file)
332         command.append("-H %s " % remote_ip)
333         command.append("-R %s " % ret_file)
334         if cipher:
335             command.append("-c %s " % cipher)
336         if cipher_key:
337             command.append("-k %s " % cipher_key)
338         if txqueuelen:
339             command.append("-q %s " % txqueuelen)
340         if bwlimit:
341             command.append("-b %s " % bwlimit)
342
343         command = " ".join(command)
344         command = self.replace_paths(command, node=node, app_home=app_home, 
345                 run_home=run_home)
346
347         return command
348
349     def establish(self, remote_endpoint, port):
350         node = self.endpoint_node(endpoint) 
351         run_home = self.run_home(endpoint)
352
353         # upload remote port number to file
354         remote_port = "%s\n" % port
355         node.upload(remote_port,
356                 os.path.join(run_home, "remote_port"),
357                 text = True, 
358                 overwrite = False)
359
360     def verify(self):
361         self.wait_result()
362