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 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
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 Connection RM
60 network = Attribute("network", "IPv4 Network Address",
61 flags = Flags.ExecReadOnly)
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,
69 flags = Flags.ExecReadOnly)
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 "
75 flags = Flags.ExecReadOnly)
77 txqueuelen = Attribute("txQueueLen",
78 "Specifies the interface's transmission queue length. "
81 flags = Flags.ExecReadOnly)
83 bwlimit = Attribute("bwLimit",
84 "Specifies the interface's emulated bandwidth in bytes "
87 flags = Flags.ExecReadOnly)
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(OVSWitch.get_rtype())
141 def check_switch_host_link(self):
142 ''' Check if the links are between switches
143 or switch-host. Return False for 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 port_endpoints(self):
166 # # Switch-Switch connection
168 # for guid in self.connections:
169 # rm = self.ec.get_resource(guid)
170 # if hasattr(rm, "create_port"):
171 # connected.append(rm)
175 # def mixed_endpoints(self):
176 # # Switch-Host connection
178 # for guid in self.connections:
179 # rm = self.ec.get_resource(guid)
180 # if hasattr(rm, "create_port"):
182 # elif hasattr(rm, "udp_connect_command"):
186 def get_node(self, endpoint):
187 # Get connected to the nodes
189 if hasattr(endpoint, "create_port"):
190 rm_list = endpoint.get_connected(OVSWitch.get_rtype())
192 rm = rm_list[0].get_connected(PlanetlabNode.get_rtype())
194 rm = endpoint.get_connected(PlanetlabNode.get_rtype())
201 endpoint = self.endpoints()
206 endpoint = self.endpoints()
210 # def check_endpoints(self):
211 # """ Check if the links are between switches
212 # or switch-host. Return False for latter.
214 # port_endpoints = self.port_endpoints()
215 # if len(port_endpoints) == 2:
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
224 if self.check_switch_host_link :
225 host0, ip0, pname0, virt_ip0, pnumber0 = endpoint1.port_info
228 host0, ip0, pname0, virt_ip0, pnumber0 = endpoint1.port_info
229 host1, ip1, pname1, virt_ip1, pnumber1 = endpoint2.port_info
231 return pname0, ip1, pnumber1
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"))
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")
246 rem_port = str(self.get_port_info( sw_endpoint,tap_endpoint))
248 # Upload the remote port in a file
249 self.node_endpoint2.upload(rem_port, rem_port_file,
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)
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,
263 # invoke connect script
264 cmd = "bash %s" % shfile
265 (out, err), proc = self.node_endpoint2.run(cmd, self.run_home(self.node_endpoint2),
267 stdout = "udp_stdout",
268 stderr = "udp_stderr")
270 # check if execution errors
272 msg = "Failed to connect endpoints"
273 self.error(msg, out, err)
274 raise RuntimeError, msg
276 msg = "Connection on host %s configured" % self.node_endpoint2.get("hostname")
279 # Wait for pid file to be generated
280 pid, ppid = self.node_endpoint2.wait_pid(self.run_home(self.node_endpoint2))
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
288 msg = " Failed to start command '%s' " % command
289 self.error(msg, out, err)
290 raise RuntimeError, msg
294 def switch_to_switch_connect(self, endpoint, rem_endpoint):
295 """ Get switch connect command
297 # Get and configure switch connection command
299 local_port_name, remote_ip, remote_port_num = self.get_port_info(endpoint, rem_endpoint)
302 switch_connect_command = endpoint.switch_connect_command(
303 local_port_name, remote_ip, remote_port_num)
304 node_endpoint = self.get_node(endpoint)
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,
313 #invoke connect script
314 cmd = "bash %s" % shfile
315 (out, err), proc = node_endpoint.run(cmd, self.run_home(node_endpoint),
317 stdout = "sw_stdout",
318 stderr = "sw_stderr")
320 # check if execution errors occured
322 msg = "Failed to connect endpoints"
323 self.error(msg, out, err)
324 raise RuntimeError, msg
327 msg = "Connection on port %s configured" % local_port_name
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 """
337 (out, err), proc = self.node_endpoint2.check_output(self.run_home(self.node_endpoint2), 'local_port')
339 local_port = int(out)
345 msg = "Couldn't retrieve local_port"
346 self.error(msg, out, err)
347 raise RuntimeError, msg
351 def switch_to_host_connect(self, sw_endpoint, host_endpoint):
352 """Link switch--> host
354 # Retrieve remote port number from sw_endpoint
355 local_port_name = sw_endpoint.get('port_name')
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)
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,
370 # Invoke connect script
371 cmd = "bash %s" % shfile
372 (out, err), proc = self.node_endpoint1.run(cmd, self.run_home(self.node_endpoint1),
374 stdout = "sw_stdout",
375 stderr = "sw_stderr")
377 # Check if execution errors occured
380 msg = "Failed to connect endpoints"
381 self.error(msg, out, err)
382 raise RuntimeError, msg
385 msg = "Connection on port %s configured" % local_port_name
388 def do_provision(self):
389 """ Provision the tunnel
391 ..note : Endpoint 1 is always a OVSPort.
392 Endpoint 2 can be either a OVSPort or a Tap
395 self.node_endpoint1 = self.get_node(self.endpoint1)
396 self.node_endpoint1.mkdir(self.run_home(self.node_endpoint1))
398 self.node_endpoint2 = self.get_node(self.endpoint2)
399 self.node_endpoint2.mkdir(self.run_home(self.node_endpoint2))
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)
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)
410 #super(OVSTunnel, self).do_provision()
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"))
418 self.ec.register_connection(self._vroute, self.tap.guid)
420 self.ec.deploy(guids=[self._vroute], group = self.deployment_group)
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)
433 #super(OVSTunnel, self).do_deploy()
435 def do_release(self):
436 """ Release the udp_tunnel on endpoint2.
437 On endpoint1 means nothing special.
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)
446 if err or proc.poll():
447 msg = " Failed to delete TAP device"
448 self.error(msg, out, err)
450 super(OVSTunnel, self).do_release()