a26f4415dc6cf72c37e361b360782c541c3ffea6
[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 OVSSwitch   
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 OVSTunnel RM 
58
59         """
60         network = Attribute("network", "IPv4 Network Address",
61                flags = Flags.Design)
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.Design)
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.Design)
76
77         txqueuelen = Attribute("txQueueLen",
78                 "Specifies the interface's transmission queue length. "
79                 "Defaults to 1000. ", 
80                 type = Types.Integer, 
81                 flags = Flags.Design)
82
83         bwlimit = Attribute("bwLimit",
84                 "Specifies the interface's emulated bandwidth in bytes "
85                 "per second.",
86                 type = Types.Integer, 
87                 flags = Flags.Design)
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 ovsswitch(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(OVSSwitch.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 the 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 get_node(self, endpoint):
166         """ Get the nodes of the endpoint
167         """
168         rm = []
169         if hasattr(endpoint, "create_port"):
170             rm_list = endpoint.get_connected(OVSSwitch.get_rtype())
171             if rm_list:
172                 rm = rm_list[0].get_connected(PlanetlabNode.get_rtype())
173         else:
174             rm = endpoint.get_connected(PlanetlabNode.get_rtype())
175
176         if rm :
177             return rm[0]
178
179     @property
180     def endpoint1(self):
181         """ Return the first endpoint : Always a Switch
182         """
183         endpoint = self.endpoints()
184         return endpoint[0]
185
186     @property
187     def endpoint2(self):
188         """ Return the second endpoint : Either a Switch or a TAP
189         """
190         endpoint = self.endpoints()
191         return endpoint[1]
192
193     def get_port_info(self, endpoint1, endpoint2):
194         #TODO : Need to change it. Really bad to have method that return different type of things !!!!!
195         """ Retrieve the port_info list for each port
196         
197         """
198         if self.check_switch_host_link :
199             host0, ip0, pname0, virt_ip0, pnumber0 = endpoint1.port_info
200             return pnumber0
201
202         host0, ip0, pname0, virt_ip0, pnumber0 = endpoint1.port_info
203         host1, ip1, pname1, virt_ip1, pnumber1 = endpoint2.port_info
204
205         return pname0, ip1, pnumber1
206     
207     def wait_local_port(self, node_endpoint):
208         """ Waits until the if_name file for the command is generated, 
209             and returns the if_name for the device """
210
211         local_port = None
212         delay = 1.0
213
214         #TODO : Need to change it with reschedule to avoid the problem 
215         #        of the order of connection
216         for i in xrange(10):
217             (out, err), proc = node_endpoint.check_output(self.run_home(node_endpoint), 'local_port')
218             if out:
219                 local_port = int(out)
220                 break
221             else:
222                 time.sleep(delay)
223                 delay = delay * 1.5
224         else:
225             msg = "Couldn't retrieve local_port"
226             self.error(msg, out, err)
227             raise RuntimeError, msg
228
229         return local_port
230
231     def connection(self, local_endpoint, rm_endpoint):
232         """ Create the connect command for each case : 
233               - Host - Switch,  
234               - Switch - Switch,  
235               - Switch - Host
236         """
237         local_node = self.get_node(local_endpoint)
238         local_node.mkdir(self.run_home(local_node))
239
240         rm_node = self.get_node(rm_endpoint)
241         rm_node.mkdir(self.run_home(rm_node))
242
243         # Host to switch
244         if self.check_switch_host_link and local_endpoint == self.endpoint2 :
245         # Collect info from rem_endpoint
246             remote_ip = socket.gethostbyname(rm_node.get("hostname"))
247
248         # Collect info from endpoint
249             local_port_file = os.path.join(self.run_home(local_node), "local_port")
250             rem_port_file = os.path.join(self.run_home(local_node), "remote_port")
251             ret_file = os.path.join(self.run_home(local_node), "ret_file")
252             cipher = self.get("cipher")
253             cipher_key = self.get("cipherKey")
254             bwlimit = self.get("bwLimit")
255             txqueuelen = self.get("txQueueLen")
256
257             rem_port = str(self.get_port_info(rm_endpoint,local_endpoint))
258    
259         # Upload the remote port in a file
260             local_node.upload(rem_port, rem_port_file,
261                  text = True,
262                  overwrite = False)
263        
264             connect_command = local_endpoint.udp_connect_command(
265                  remote_ip, local_port_file, rem_port_file,
266                  ret_file, cipher, cipher_key, bwlimit, txqueuelen) 
267
268             self.connection_command(connect_command, local_node, rm_node)
269
270         # Wait for pid file to be generated
271             self._pid, self._ppid = local_node.wait_pid(self.run_home(local_node))
272
273             if not self._pid or not self._ppid:
274                 (out, err), proc = local_node.check_errors(self.run_home(local_node))
275                 # Out is what was written in the stderr file
276                 if err:
277                     msg = " Failed to start connection of the OVS Tunnel "
278                     self.error(msg, out, err)
279                     raise RuntimeError, msg
280             return
281
282         # Switch to Host
283         if self.check_switch_host_link and local_endpoint == self.endpoint1:
284             local_port_name = local_endpoint.get('port_name')
285             remote_port_num = self.wait_local_port(rm_node)
286             remote_ip = socket.gethostbyname(rm_node.get("hostname"))
287   
288         # Switch to Switch
289         if not self.check_switch_host_link :
290             local_port_name, remote_ip, remote_port_num = self.get_port_info(local_endpoint, rm_endpoint)
291
292         connect_command = local_endpoint.switch_connect_command(
293                     local_port_name, remote_ip, remote_port_num)
294
295         self.connection_command(connect_command, local_node, rm_node)       
296
297     def connection_command(self, command, node_endpoint, rm_node_endpoint):
298         """ Execute the connection command on the node and check if the processus is
299             correctly running on the node.
300         """
301         shfile = os.path.join(self.app_home(node_endpoint), "sw_connect.sh")
302         node_endpoint.upload(command,
303                 shfile,
304                 text = True,
305                 overwrite = False)
306
307         # Invoke connect script
308         out = err= ''       
309         cmd = "bash %s" % shfile
310         (out, err), proc = node_endpoint.run(cmd, self.run_home(node_endpoint),
311                 sudo  = True,
312                 stdout = "sw_stdout",
313                 stderr = "sw_stderr")
314         
315         # Check if execution errors occured
316
317         if proc.poll():
318             msg = "Failed to connect endpoints"
319             self.error(msg, out, err)
320             raise RuntimeError, msg
321
322         # For debugging
323         msg = "Connection on port configured"
324         self.debug(msg)
325
326     def do_provision(self):
327         """ Provision the tunnel
328         """
329         
330         #TODO : The order of the connection is important for now ! 
331         # Need to change the code of wait local port
332         self.connection(self.endpoint2, self.endpoint1)
333         self.connection(self.endpoint1, self.endpoint2)
334
335     def configure_route(self):
336         """ Configure the route for the tap device
337
338             .. note : In case of a conection between a switch and a host, a route
339                       was missing on the node with the Tap Device. This method create
340                       the missing route. 
341         """
342
343         if  self.check_switch_host_link:
344             self._vroute = self.ec.register_resource("PlanetlabVroute")
345             self.ec.set(self._vroute, "action", "add")
346             self.ec.set(self._vroute, "network", self.get("network"))
347
348             self.ec.register_connection(self._vroute, self.tap.guid)
349             self.ec.deploy(guids=[self._vroute], group = self.deployment_group)
350
351     def do_deploy(self):
352         """ Deploy the tunnel after the endpoint get ready
353         """
354         if (not self.endpoint1 or self.endpoint1.state < ResourceState.READY) or \
355             (not self.endpoint2 or self.endpoint2.state < ResourceState.READY):
356             self.ec.schedule(reschedule_delay, self.deploy)
357             return
358
359         self.do_discover()
360         self.do_provision()
361         self.configure_route()
362
363         # Cannot call the deploy of the linux application 
364         #         because of a log error.
365         # Need to investigate if it is right that the tunnel 
366         #    inherits from the linux application
367         #  super(OVSTunnel, self).do_deploy()
368         self.set_ready()
369  
370     def do_release(self):
371         """ Release the tunnel by releasing the Tap Device if exists
372         """
373         if self.check_switch_host_link:
374             # TODO: Make more generic Release method of PLTAP
375             tap_node = self.get_node(self.endpoint2)
376             if self._pid and self._ppid:
377                 (out, err), proc = tap_node.kill(self._pid,
378                         self._ppid, sudo = True)
379
380                 if err or proc.poll():
381                     msg = " Failed to delete TAP device"
382                     self.error(msg, out, err)
383
384         super(OVSTunnel, self).do_release()
385