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