2 # NEPI, a framework to manage network experiments
3 # Copyright (C) 2013 INRIA
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.
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.
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/>.
18 # Author: Alina Quereilhac <alina.quereilhac@inria.fr>
19 # Alexandros Kouvakas <alexandros.kouvakas@gmail.com>
22 from nepi.execution.attribute import Attribute, Flags, Types
23 from nepi.execution.resource import ResourceManager, ResourceFactory, clsinit_copy, \
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
36 reschedule_delay = "0.5s"
39 class OVSTunnel(LinuxApplication):
41 .. class:: Class Args :
43 :param ec: The Experiment controller
44 :type ec: ExperimentController
45 :param guid: guid of the RM
47 :param creds: Credentials to communicate with the rm
53 _authorized_connections = ["OVSPort", "PlanetlabTap"]
56 def _register_attributes(cls):
57 """ Register the attributes of OVSTunnel RM
60 network = Attribute("network", "IPv4 Network Address",
63 cipher = Attribute("cipher",
64 "Cipher to encript communication. "
65 "One of PLAIN, AES, Blowfish, DES, DES3. ",
67 allowed = ["PLAIN", "AES", "Blowfish", "DES", "DES3"],
68 type = Types.Enumerate,
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 "
77 txqueuelen = Attribute("txQueueLen",
78 "Specifies the interface's transmission queue length. "
83 bwlimit = Attribute("bwLimit",
84 "Specifies the interface's emulated bandwidth in bytes "
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)
95 def __init__(self, ec, guid):
97 :param ec: The Experiment controller
98 :type ec: ExperimentController
99 :param guid: guid of the RM
103 super(OVSTunnel, self).__init__(ec, guid)
104 self._home = "tunnel-%s" % self.guid
105 self.port_info_tunl = []
109 self._node_endpoint1 = None
110 self._node_endpoint2 = None
112 def log_message(self, msg):
113 return " guid %d - Tunnel - %s " % (self.guid, msg)
115 def app_home(self, node):
116 return os.path.join(node.exp_home, self._home)
118 def run_home(self, node):
119 return os.path.join(self.app_home(node), self.ec.run_id)
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):
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())
141 def check_switch_host_link(self):
142 """ Check if the links are between switches
143 or switch-host. Return False for the latter.
151 """ Return the list with the two connected elements.
152 Either Switch-Switch or Switch-Host
156 for guid in self.connections:
157 rm = self.ec.get_resource(guid)
158 if hasattr(rm, "create_port"):
159 connected[position] = rm
161 elif hasattr(rm, "udp_connect_command"):
165 def get_node(self, endpoint):
166 """ Get the nodes of the endpoint
169 if hasattr(endpoint, "create_port"):
170 rm_list = endpoint.get_connected(OVSSwitch.get_rtype())
172 rm = rm_list[0].get_connected(PlanetlabNode.get_rtype())
174 rm = endpoint.get_connected(PlanetlabNode.get_rtype())
181 """ Return the first endpoint : Always a Switch
183 endpoint = self.endpoints()
188 """ Return the second endpoint : Either a Switch or a TAP
190 endpoint = self.endpoints()
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
198 if self.check_switch_host_link :
199 host0, ip0, pname0, virt_ip0, pnumber0 = endpoint1.port_info
202 host0, ip0, pname0, virt_ip0, pnumber0 = endpoint1.port_info
203 host1, ip1, pname1, virt_ip1, pnumber1 = endpoint2.port_info
205 return pname0, ip1, pnumber1
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 """
214 #TODO : Need to change it with reschedule to avoid the problem
215 # of the order of connection
217 (out, err), proc = node_endpoint.check_output(self.run_home(node_endpoint), 'local_port')
219 local_port = int(out)
225 msg = "Couldn't retrieve local_port"
226 self.error(msg, out, err)
227 raise RuntimeError, msg
231 def connection(self, local_endpoint, rm_endpoint):
232 """ Create the connect command for each case :
237 local_node = self.get_node(local_endpoint)
238 local_node.mkdir(self.run_home(local_node))
240 rm_node = self.get_node(rm_endpoint)
241 rm_node.mkdir(self.run_home(rm_node))
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"))
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")
257 rem_port = str(self.get_port_info(rm_endpoint,local_endpoint))
259 # Upload the remote port in a file
260 local_node.upload(rem_port, rem_port_file,
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)
268 self.connection_command(connect_command, local_node, rm_node)
270 # Wait for pid file to be generated
271 self._pid, self._ppid = local_node.wait_pid(self.run_home(local_node))
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
277 msg = " Failed to start connection of the OVS Tunnel "
278 self.error(msg, out, err)
279 raise RuntimeError, msg
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"))
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)
292 connect_command = local_endpoint.switch_connect_command(
293 local_port_name, remote_ip, remote_port_num)
295 self.connection_command(connect_command, local_node, rm_node)
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.
301 shfile = os.path.join(self.app_home(node_endpoint), "sw_connect.sh")
302 node_endpoint.upload(command,
307 # Invoke connect script
309 cmd = "bash %s" % shfile
310 (out, err), proc = node_endpoint.run(cmd, self.run_home(node_endpoint),
312 stdout = "sw_stdout",
313 stderr = "sw_stderr")
315 # Check if execution errors occured
318 msg = "Failed to connect endpoints"
319 self.error(msg, out, err)
320 raise RuntimeError, msg
323 msg = "Connection on port configured"
326 def do_provision(self):
327 """ Provision the tunnel
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)
335 def configure_route(self):
336 """ Configure the route for the tap device
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
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"))
348 self.ec.register_connection(self._vroute, self.tap.guid)
349 self.ec.deploy(guids=[self._vroute], group = self.deployment_group)
352 """ Deploy the tunnel after the endpoint get ready
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)
361 self.configure_route()
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()
370 def do_release(self):
371 """ Release the tunnel by releasing the Tap Device if exists
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)
380 if err or proc.poll():
381 msg = " Failed to delete TAP device"
382 self.error(msg, out, err)
384 super(OVSTunnel, self).do_release()