Updated example scripts (Fedora repo update + ovsswitch)
[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         port = self.initiate(endpoint, remote_endpoint, address, cipher, 
189                 cipher_key, bwlimit, txqueuelen)
190
191         return port
192
193     def establish_connection(self, endpoint, remote_endpoint, port):
194         self.establish(endpoint, remote_endpoint, port)
195
196     def verify_connection(self, endpoint, remote_endpoint):
197         self.verify(endpoint)
198
199     def terminate_connection(self, endpoint, remote_endpoint):
200         # Nothing to do
201         return
202
203     def check_state_connection(self):
204         # Make sure the process is still running in background
205         # No execution errors occurred. Make sure the background
206         # process with the recorded pid is still running.
207
208         node1 = self.endpoint_node(self.endpoint1) 
209         node2 = self.endpoint_node(self.endpoint2) 
210         run_home1 = self.run_home(self.endpoint1)
211         run_home2 = self.run_home(self.endpoint1)
212         (pid1, ppid1) = self._pids[endpoint1]
213         (pid2, ppid2) = self._pids[endpoint2]
214         
215         status1 = node1.status(pid1, ppid1)
216         status2 = node2.status(pid2, ppid2)
217
218         if status1 == ProcStatus.FINISHED and \
219                 status2 == ProcStatus.FINISHED:
220
221             # check if execution errors occurred
222             (out1, err1), proc1 = node1.check_errors(run_home1)
223             (out2, err2), proc2 = node2.check_errors(run_home2)
224
225             if err1 or err2: 
226                 msg = "Error occurred in tunnel"
227                 self.error(msg, err1, err2)
228                 self.fail()
229             else:
230                 self.set_stopped()
231
232     def wait_local_port(self, endpoint):
233         """ Waits until the local_port file for the endpoint is generated, 
234         and returns the port number 
235         
236         """
237         return self.wait_file(endpoint, "local_port")
238
239     def wait_result(self, endpoint):
240         """ Waits until the return code file for the endpoint is generated 
241         
242         """ 
243         return self.wait_file(endpoint, "ret_file")
244  
245     def wait_file(self, endpoint, filename):
246         """ Waits until file on endpoint is generated """
247         result = None
248         delay = 1.0
249         
250         node = self.endpoint_node(endpoint) 
251         run_home = self.run_home(endpoint)
252
253         for i in xrange(20):
254             (out, err), proc = node.check_output(run_home, filename)
255
256             if out:
257                 result = out.strip()
258                 break
259             else:
260                 time.sleep(delay)
261                 delay = delay * 1.5
262         else:
263             msg = "Couldn't retrieve %s" % filename
264             self.error(msg, out, err)
265             raise RuntimeError, msg
266
267         return result
268
269     def initiate(self, endpoint, remote_endpoint, address, cipher, cipher_key, 
270             bwlimit, txqueuelen):
271
272         command = self._initiate_command(endpoint, remote_endpoint, 
273                 address, 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, remote_endpoint, address,
314             cipher, cipher_key, bwlimit, txqueuelen):
315         local_node = self.endpoint_node(endpoint) 
316         local_run_home = self.run_home(endpoint)
317         local_app_home = self.app_home(endpoint)
318         remote_node = self.endpoint_node(remote_endpoint) 
319
320         local_ip = local_node.get("ip")
321         remote_ip = remote_node.get("ip")
322
323         local_port_file = os.path.join(local_run_home, "local_port")
324         remote_port_file = os.path.join(local_run_home,  "remote_port")
325         ret_file = os.path.join(local_run_home, "ret_file")
326
327         address = base64.b64encode(address)
328         
329         command = [""]
330         # Use pl-vid-udp-connect.py to stablish the tunnel between endpoints
331         command.append("PYTHONPATH=$PYTHONPATH:${SRC}")
332         command.append("python ${SRC}/linux-ns3-fd-udp-connect.py")
333         command.append("-a %s" % address)
334         command.append("-p %s " % local_port_file)
335         command.append("-P %s " % remote_port_file)
336         command.append("-o %s " % local_ip)
337         command.append("-O %s " % remote_ip)
338         command.append("-R %s " % ret_file)
339         if cipher:
340             command.append("-c %s " % cipher)
341         if cipher_key:
342             command.append("-k %s " % cipher_key)
343         if txqueuelen:
344             command.append("-q %s " % txqueuelen)
345         if bwlimit:
346             command.append("-b %s " % bwlimit)
347
348         command = " ".join(command)
349         command = self.replace_paths(command, node=local_node, 
350                 app_home=local_app_home, run_home=local_run_home)
351
352         return command
353
354     def establish(self, endpoint, remote_endpoint, port):
355         node = self.endpoint_node(endpoint) 
356         run_home = self.run_home(endpoint)
357
358         # upload remote port number to file
359         remote_port = "%s\n" % port
360         node.upload(remote_port,
361                 os.path.join(run_home, "remote_port"),
362                 text = True, 
363                 overwrite = False)
364
365     def verify(self, endpoint):
366         self.wait_result(endpoint)
367