c9c27119376677aac4eb0b63b6ed81f61711f0fb
[nepi.git] / src / nepi / resources / planetlab / openvswitch / tunnel.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 #             Alexandros Kouvakas <alexandros.kouvakas@gmail.com>
20
21
22 from nepi.execution.attribute import Attribute, Flags, Types
23 from nepi.execution.resource import ResourceManager, ResourceFactory, clsinit_copy, \
24         ResourceState
25 from nepi.resources.linux.application import LinuxApplication
26 from nepi.resources.planetlab.node import PlanetlabNode            
27 from nepi.resources.planetlab.openvswitch.ovs import OVSWitch   
28 from nepi.util.timefuncs import tnow, tdiffsec    
29 from nepi.resources.planetlab.vroute import PlanetlabVroute
30 from nepi.resources.planetlab.tap import PlanetlabTap
31
32 import os
33 import time
34 import socket
35
36 reschedule_delay = "0.5s"
37
38 @clsinit_copy                 
39 class OVSTunnel(LinuxApplication):
40     """
41     .. class:: Class Args :
42       
43         :param ec: The Experiment controller
44         :type ec: ExperimentController
45         :param guid: guid of the RM
46         :type guid: int
47         :param creds: Credentials to communicate with the rm 
48         :type creds: dict
49
50     """
51     
52     _rtype = "OVSTunnel"
53     _authorized_connections = ["OVSPort", "PlanetlabTap"]    
54
55     @classmethod
56     def _register_attributes(cls):
57         """ Register the attributes of Connection RM 
58
59         """
60         network = Attribute("network", "IPv4 Network Address",
61                flags = Flags.ExecReadOnly)
62
63         cipher = Attribute("cipher",
64                "Cipher to encript communication. "
65                 "One of PLAIN, AES, Blowfish, DES, DES3. ",
66                 default = None,
67                 allowed = ["PLAIN", "AES", "Blowfish", "DES", "DES3"],
68                 type = Types.Enumerate, 
69                 flags = Flags.ExecReadOnly)
70
71         cipher_key = Attribute("cipherKey",
72                 "Specify a symmetric encryption key with which to protect "
73                 "packets across the tunnel. python-crypto must be installed "
74                 "on the system." ,
75                 flags = Flags.ExecReadOnly)
76
77         txqueuelen = Attribute("txQueueLen",
78                 "Specifies the interface's transmission queue length. "
79                 "Defaults to 1000. ", 
80                 type = Types.Integer, 
81                 flags = Flags.ExecReadOnly)
82
83         bwlimit = Attribute("bwLimit",
84                 "Specifies the interface's emulated bandwidth in bytes "
85                 "per second.",
86                 type = Types.Integer, 
87                 flags = Flags.ExecReadOnly)
88
89         cls._register_attribute(network)
90         cls._register_attribute(cipher)
91         cls._register_attribute(cipher_key)
92         cls._register_attribute(txqueuelen)
93         cls._register_attribute(bwlimit)
94
95     def __init__(self, ec, guid):
96         """
97         :param ec: The Experiment controller
98         :type ec: ExperimentController
99         :param guid: guid of the RM
100         :type guid: int
101     
102         """
103         super(OVSTunnel, self).__init__(ec, guid)
104         self._home = "tunnel-%s" % self.guid
105         self.port_info_tunl = []
106         self._pid = None
107         self._ppid = None
108         self._vroute = None
109         self._node_endpoint1 = None
110         self._node_endpoint2 = None
111
112     def log_message(self, msg):
113         return " guid %d - Tunnel - %s " % (self.guid, msg)
114
115     def app_home(self, node):
116         return os.path.join(node.exp_home, self._home)
117
118     def run_home(self, node):
119         return os.path.join(self.app_home(node), self.ec.run_id)
120
121     @property
122     def tap(self):
123         ''' Return the Tap RM if it exists '''
124         rclass = ResourceFactory.get_resource_type(PlanetlabTap.get_rtype())
125         for guid in self.connections:
126             rm = self.ec.get_resource(guid)
127             if isinstance(rm, rclass):
128                 return rm
129
130     @property
131     def ovswitch(self):
132         ''' Return the 1st switch '''
133         for guid in self.connections:
134             rm_port = self.ec.get_resource(guid)
135             if hasattr(rm_port, "create_port"):
136                 rm_list = rm_port.get_connected(OVSWitch.get_rtype())
137                 if rm_list:
138                     return rm_list[0]
139
140     @property         
141     def check_switch_host_link(self):
142         ''' Check if the links are between switches
143             or switch-host. Return False for latter.
144         '''
145         if self.tap :
146             return True
147         return False
148
149
150     def endpoints(self):
151         ''' Return the list with the two connected elements.
152         Either Switch-Switch or Switch-Host
153         '''
154         connected = [1, 1]
155         position = 0
156         for guid in self.connections:
157             rm = self.ec.get_resource(guid)
158             if hasattr(rm, "create_port"):
159                 connected[position] = rm
160                 position += 1
161             elif hasattr(rm, "udp_connect_command"):
162                 connected[1] = rm
163         return connected
164
165 #    def port_endpoints(self):
166 #        # Switch-Switch connection
167 #        connected = []
168 #        for guid in self.connections:
169 #            rm = self.ec.get_resource(guid)
170 #            if hasattr(rm, "create_port"):
171 #                connected.append(rm)
172 #        return connected
173
174 #    
175 #    def mixed_endpoints(self):
176 #        # Switch-Host connection
177 #        connected = [1, 2]
178 #        for guid in self.connections:
179 #            rm = self.ec.get_resource(guid)
180 #            if hasattr(rm, "create_port"):
181 #                connected[0] = rm
182 #            elif hasattr(rm, "udp_connect_command"):
183 #                connected[1] = rm
184 #        return connected
185
186     def get_node(self, endpoint):
187         # Get connected to the nodes
188         rm = []
189         if hasattr(endpoint, "create_port"):
190             rm_list = endpoint.get_connected(OVSWitch.get_rtype())
191             if rm_list:
192                 rm = rm_list[0].get_connected(PlanetlabNode.get_rtype())
193         else:
194             rm = endpoint.get_connected(PlanetlabNode.get_rtype())
195
196         if rm :
197             return rm[0]
198
199     @property
200     def endpoint1(self):
201             endpoint = self.endpoints()
202             return endpoint[0]
203
204     @property
205     def endpoint2(self):
206             endpoint = self.endpoints()
207             return endpoint[1]
208
209 #    @property          
210 #    def check_endpoints(self):
211 #        """ Check if the links are between switches
212 #            or switch-host. Return False for latter.
213 #        """
214 #        port_endpoints = self.port_endpoints()
215 #        if len(port_endpoints) == 2:
216 #            return True
217 #        return False
218
219     def get_port_info(self, endpoint1, endpoint2):
220         # Need to change it. Not good to have method that return different type of things !!!!!
221         """ Retrieve the port_info list for each port
222         
223         """
224         if self.check_switch_host_link :
225             host0, ip0, pname0, virt_ip0, pnumber0 = endpoint1.port_info
226             return pnumber0
227
228         host0, ip0, pname0, virt_ip0, pnumber0 = endpoint1.port_info
229         host1, ip1, pname1, virt_ip1, pnumber1 = endpoint2.port_info
230
231         return pname0, ip1, pnumber1
232     
233     def host_to_switch_connect(self, tap_endpoint, sw_endpoint):     
234         # Collect info from rem_endpoint
235         remote_ip = socket.gethostbyname(self.node_endpoint1.get("hostname"))
236
237         # Collect info from endpoint
238         local_port_file = os.path.join(self.run_home(self.node_endpoint2), "local_port")
239         rem_port_file = os.path.join(self.run_home(self.node_endpoint2), "remote_port")
240         ret_file = os.path.join(self.run_home(self.node_endpoint2), "ret_file")
241         cipher = self.get("cipher")
242         cipher_key = self.get("cipherKey")
243         bwlimit = self.get("bwLimit")
244         txqueuelen = self.get("txQueueLen")
245
246         rem_port = str(self.get_port_info( sw_endpoint,tap_endpoint))
247
248         # Upload the remote port in a file
249         self.node_endpoint2.upload(rem_port, rem_port_file,
250                 text = True,
251                 overwrite = False)
252        
253         udp_connect_command = tap_endpoint.udp_connect_command(
254                 remote_ip, local_port_file, rem_port_file,
255                 ret_file, cipher, cipher_key, bwlimit, txqueuelen) 
256
257         # upload command to host_connect.sh script
258         shfile = os.path.join(self.app_home(self.node_endpoint2), "host_connect.sh")
259         self.node_endpoint2.upload(udp_connect_command, shfile,
260                 text = True,
261                 overwrite = False)
262
263         # invoke connect script
264         cmd = "bash %s" % shfile
265         (out, err), proc = self.node_endpoint2.run(cmd, self.run_home(self.node_endpoint2),
266                 sudo  = True,
267                 stdout = "udp_stdout",
268                 stderr = "udp_stderr")
269
270         # check if execution errors
271         if proc.poll():
272             msg = "Failed to connect endpoints"
273             self.error(msg, out, err)
274             raise RuntimeError, msg
275
276         msg = "Connection on host %s configured" % self.node_endpoint2.get("hostname")
277         self.debug(msg)
278          
279         # Wait for pid file to be generated
280         pid, ppid = self.node_endpoint2.wait_pid(self.run_home(self.node_endpoint2))
281         
282         # If the process is not running, check for error information
283         # on the remote machine
284         if not pid or not ppid:
285             (out, err), proc = self.node_endpoint2.check_errors(self.run_home(self.node_endpoint2))
286             # Out is what was written in the stderr file
287             if err:
288                 msg = " Failed to start command '%s' " % command
289                 self.error(msg, out, err)
290                 raise RuntimeError, msg
291                 
292         return (pid, ppid)
293
294     def switch_to_switch_connect(self, endpoint, rem_endpoint):
295         """ Get switch connect command
296         """
297         # Get and configure switch connection command
298
299         local_port_name, remote_ip, remote_port_num = self.get_port_info(endpoint, rem_endpoint)
300
301
302         switch_connect_command = endpoint.switch_connect_command(
303                 local_port_name, remote_ip, remote_port_num)
304         node_endpoint = self.get_node(endpoint)        
305
306         # Upload command to the file sw_connect.sh
307         shfile = os.path.join(self.app_home(node_endpoint), "sw_connect.sh")
308         node_endpoint.upload(switch_connect_command,
309                 shfile,
310                 text = True,
311                 overwrite = False)
312
313         #invoke connect script
314         cmd = "bash %s" % shfile
315         (out, err), proc = node_endpoint.run(cmd, self.run_home(node_endpoint),
316                 sudo  = True,
317                 stdout = "sw_stdout",
318                 stderr = "sw_stderr")
319         
320         # check if execution errors occured
321         if proc.poll():
322             msg = "Failed to connect endpoints"
323             self.error(msg, out, err)
324             raise RuntimeError, msg
325
326         # For debugging
327         msg = "Connection on port %s configured" % local_port_name
328         self.info(msg)
329
330     def wait_local_port(self):
331         """ Waits until the if_name file for the command is generated, 
332             and returns the if_name for the device """
333         local_port = None
334         delay = 1.0
335
336         for i in xrange(10):
337             (out, err), proc = self.node_endpoint2.check_output(self.run_home(self.node_endpoint2), 'local_port')
338             if out:
339                 local_port = int(out)
340                 break
341             else:
342                 time.sleep(delay)
343                 delay = delay * 1.5
344         else:
345             msg = "Couldn't retrieve local_port"
346             self.error(msg, out, err)
347             raise RuntimeError, msg
348
349         return local_port
350
351     def switch_to_host_connect(self, sw_endpoint, host_endpoint):
352         """Link switch--> host
353         """
354         # Retrieve remote port number from sw_endpoint
355         local_port_name = sw_endpoint.get('port_name')
356
357         out = err= ''
358         remote_port_num = self.wait_local_port()
359         remote_ip = socket.gethostbyname(self.node_endpoint2.get("hostname"))
360         switch_connect_command = sw_endpoint.switch_connect_command(
361                 local_port_name, remote_ip, remote_port_num)
362
363         # Upload command to the file sw_connect.sh
364         shfile = os.path.join(self.app_home(self.node_endpoint1), "sw_connect.sh")
365         self.node_endpoint1.upload(switch_connect_command,
366                 shfile,
367                 text = True,
368                 overwrite = False)
369
370         # Invoke connect script
371         cmd = "bash %s" % shfile
372         (out, err), proc = self.node_endpoint1.run(cmd, self.run_home(self.node_endpoint1),
373                 sudo  = True,
374                 stdout = "sw_stdout",
375                 stderr = "sw_stderr")
376         
377         # Check if execution errors occured
378
379         if proc.poll():
380             msg = "Failed to connect endpoints"
381             self.error(msg, out, err)
382             raise RuntimeError, msg
383
384         # For debugging
385         msg = "Connection on port %s configured" % local_port_name
386         self.debug(msg)                                                   
387
388     def do_provision(self):
389         """ Provision the tunnel
390
391            ..note : Endpoint 1 is always a OVSPort. 
392                     Endpoint 2 can be either a OVSPort or a Tap
393                      
394         """
395         self.node_endpoint1 = self.get_node(self.endpoint1)
396         self.node_endpoint1.mkdir(self.run_home(self.node_endpoint1))
397
398         self.node_endpoint2 = self.get_node(self.endpoint2)
399         self.node_endpoint2.mkdir(self.run_home(self.node_endpoint2))
400
401         if not self.check_switch_host_link:
402             # Invoke connect script between switches
403             self.switch_to_switch_connect(self.endpoint1, self.endpoint2)
404             self.switch_to_switch_connect(self.endpoint2, self.endpoint1)
405         else: 
406             # Invoke connect script between switch & host
407             (self._pid, self._ppid) = self.host_to_switch_connect(self.endpoint2, self.endpoint1)
408             self.switch_to_host_connect(self.endpoint1, self.endpoint2)
409
410         #super(OVSTunnel, self).do_provision()
411
412     def configure(self):
413         if  self.check_switch_host_link:
414             self._vroute = self.ec.register_resource("PlanetlabVroute")
415             self.ec.set(self._vroute, "action", "add")
416             self.ec.set(self._vroute, "network", self.get("network"))
417
418             self.ec.register_connection(self._vroute, self.tap.guid)
419             # schedule deploy
420             self.ec.deploy(guids=[self._vroute], group = self.deployment_group)
421
422     def do_deploy(self):
423         if (not self.endpoint1 or self.endpoint1.state < ResourceState.READY) or \
424             (not self.endpoint2 or self.endpoint2.state < ResourceState.READY):
425             self.ec.schedule(reschedule_delay, self.deploy)
426             return
427
428         self.do_discover()
429         self.do_provision()
430         self.configure()
431
432         self.set_ready()
433         #super(OVSTunnel, self).do_deploy()
434  
435     def do_release(self):
436         """ Release the udp_tunnel on endpoint2.
437             On endpoint1 means nothing special.        
438         """
439         if not self.check_switch_host_link:
440             # Kill the TAP devices
441             # TODO: Make more generic Release method of PLTAP
442             if self._pid and self._ppid:
443                 (out, err), proc = self.node_enpoint2.kill(self._pid,
444                         self._ppid, sudo = True)
445
446                 if err or proc.poll():
447                     msg = " Failed to delete TAP device"
448                     self.error(msg, out, err)
449
450         super(OVSTunnel, self).do_release()
451