Refactoring LinuGRETunnel
[nepi.git] / src / nepi / resources / linux / tap.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
20 from nepi.execution.attribute import Attribute, Flags, Types
21 from nepi.execution.resource import clsinit_copy, ResourceState, \
22         reschedule_delay
23 from nepi.resources.linux.application import LinuxApplication
24 from nepi.resources.linux.node import LinuxNode
25 from nepi.util.timefuncs import tnow, tdiffsec
26
27 import os
28 import socket
29 import time
30
31 PYTHON_VSYS_VERSION = "1.0"
32
33 @clsinit_copy
34 class LinuxTap(LinuxApplication):
35     _rtype = "LinuxTap"
36     _help = "Creates a TAP device on a Linux host"
37     _backend = "linux"
38
39     @classmethod
40     def _register_attributes(cls):
41         ip4 = Attribute("ip4", "IPv4 Address",
42               flags = Flags.Design)
43
44         mac = Attribute("mac", "MAC Address",
45                 flags = Flags.Design)
46
47         prefix4 = Attribute("prefix4", "IPv4 network prefix",
48                 type = Types.Integer,
49                 flags = Flags.Design)
50
51         mtu = Attribute("mtu", "Maximum transmition unit for device",
52                 type = Types.Integer)
53
54         devname = Attribute("deviceName", 
55                 "Name of the network interface (e.g. eth0, wlan0, etc)",
56                 flags = Flags.NoWrite)
57
58         up = Attribute("up", "Link up", 
59                 type = Types.Bool)
60         
61         pointopoint = Attribute("pointopoint", "Peer IP address", 
62                 flags = Flags.Design)
63
64         txqueuelen = Attribute("txqueuelen", "Length of transmission queue", 
65                 flags = Flags.Design)
66
67         txqueuelen = Attribute("txqueuelen", "Length of transmission queue", 
68                 flags = Flags.Design)
69
70         gre_key = Attribute("greKey", 
71                 "GRE key to be used to configure GRE tunnel", 
72                 default = "1",
73                 flags = Flags.Design)
74
75         gre_remote = Attribute("greRemote", 
76                 "Public IP of remote endpoint for GRE tunnel", 
77                 flags = Flags.Design)
78
79         tear_down = Attribute("tearDown", 
80                 "Bash script to be executed before releasing the resource",
81                 flags = Flags.Design)
82
83         cls._register_attribute(ip4)
84         cls._register_attribute(mac)
85         cls._register_attribute(prefix4)
86         cls._register_attribute(mtu)
87         cls._register_attribute(devname)
88         cls._register_attribute(up)
89         cls._register_attribute(pointopoint)
90         cls._register_attribute(txqueuelen)
91         cls._register_attribute(gre_key)
92         cls._register_attribute(gre_remote)
93         cls._register_attribute(tear_down)
94
95     def __init__(self, ec, guid):
96         super(LinuxTap, self).__init__(ec, guid)
97         self._home = "tap-%s" % self.guid
98         self._gre_enabled = False
99
100     @property
101     def node(self):
102         node = self.get_connected(LinuxNode.get_rtype())
103         if node: return node[0]
104         return None
105
106     @property
107     def gre_enabled(self):
108         if not self._gre_enabled:
109             from nepi.resources.linux.gretunnel import LinuxGRETunnel
110             gre = self.get_connected(LinuxGRETunnel.get_rtype())
111             if gre: self._gre_enabled = True
112
113         return self._gre_enabled
114
115     def upload_sources(self):
116         # upload stop.sh script
117         stop_command = self.replace_paths(self._stop_command)
118
119         self.node.upload(stop_command,
120                 os.path.join(self.app_home, "stop.sh"),
121                 text = True,
122                 # Overwrite file every time. 
123                 # The stop.sh has the path to the socket, which should change
124                 # on every experiment run.
125                 overwrite = True)
126
127     def upload_start_command(self):
128         # If GRE mode is enabled, TAP creation is delayed until the
129         # tunnel is established
130         if not self.gre_enabled:
131             # We want to make sure the device is up and running
132             # before the deploy is over, so we execute the 
133             # start script now and wait until it finishes. 
134             command = self.get("command")
135             command = self.replace_paths(command)
136
137             shfile = os.path.join(self.app_home, "start.sh")
138             self.node.run_and_wait(command, self.run_home,
139                 shfile = shfile,
140                 overwrite = True)
141
142     def do_deploy(self):
143         if not self.node or self.node.state < ResourceState.PROVISIONED:
144             self.ec.schedule(reschedule_delay, self.deploy)
145         else:
146             if not self.get("deviceName"):
147                 self.set("deviceName", "%s%d" % (self.vif_prefix, self.guid)) 
148
149             if not self.get("command"):
150                 self.set("command", self._start_command)
151
152             self.do_discover()
153             self.do_provision()
154
155             self.set_ready()
156
157     def do_start(self):
158         if self.state == ResourceState.READY:
159             command = self.get("command")
160             self.info("Starting command '%s'" % command)
161
162             self.set_started()
163         else:
164             msg = " Failed to execute command '%s'" % command
165             self.error(msg, out, err)
166             raise RuntimeError, msg
167
168     def do_stop(self):
169         command = self.get('command') or ''
170         
171         if self.state == ResourceState.STARTED:
172             self.info("Stopping command '%s'" % command)
173
174             command = "bash %s" % os.path.join(self.app_home, "stop.sh")
175             (out, err), proc = self.execute_command(command,
176                     blocking = True)
177
178             if err:
179                 msg = " Failed to stop command '%s' " % command
180                 self.error(msg, out, err)
181
182             self.set_stopped()
183
184     @property
185     def state(self):
186         state_check_delay = 0.5
187         if self._state == ResourceState.STARTED and \
188                 tdiffsec(tnow(), self._last_state_check) > state_check_delay:
189
190             if self.get("deviceName"):
191                 (out, err), proc = self.node.execute("ifconfig")
192
193                 if out.strip().find(self.get("deviceName")) == -1: 
194                     # tap is not running is not running (socket not found)
195                     self.set_stopped()
196
197             self._last_state_check = tnow()
198
199         return self._state
200
201     def do_release(self):
202         # Node needs to wait until all associated RMs are released
203         # to be released
204         from nepi.resources.linux.tunnel import LinuxTunnel
205         rms = self.get_connected(LinuxTunnel.get_rtype())
206
207         for rm in rms:
208             if rm.state < ResourceState.STOPPED:
209                 self.ec.schedule(reschedule_delay, self.release)
210                 return 
211
212         super(LinuxTap, self).do_release()
213
214     def gre_connect(self, remote_endpoint, connection_app_home,
215             connection_run_home):
216         gre_connect_command = self._gre_connect_command(
217                 remote_endpoint, connection_run_home)
218
219         # upload command to connect.sh script
220         shfile = os.path.join(connection_app_home, "gre-connect.sh")
221         endpoint.node.upload(gre_connect_command,
222                 shfile,
223                 text = True, 
224                 overwrite = False)
225
226         # invoke connect script
227         cmd = "bash %s" % shfile
228         (out, err), proc = self.node.run(cmd, connection_run_home) 
229              
230         # check if execution errors occurred
231         msg = " Failed to connect endpoints "
232         
233         if proc.poll() or err:
234             self.error(msg, out, err)
235             raise RuntimeError, msg
236     
237         # Wait for pid file to be generated
238         pid, ppid = self.node.wait_pid(connection_run_home)
239         
240         # If the process is not running, check for error information
241         # on the remote machine
242         if not pid or not ppid:
243             (out, err), proc = self.node.check_errors(connection_run_home)
244             # Out is what was written in the stderr file
245             if err:
246                 msg = " Failed to start command '%s' " % command
247                 self.error(msg, out, err)
248                 raise RuntimeError, msg
249         
250         return True
251
252     def _gre_connect_command(self, remote_endpoint, connection_run_home): 
253         # Set the remote endpoint
254         self.set("pointopoint", remote_endpoint.get("ip4"))
255         self.set("greRemote", socket.gethostbyname(
256             remote_endpoint.node.get("hostname")))
257
258         # Generate GRE connect command
259         command = ["("]
260         command.append(self._stop_command)
261         command.append(") ; (")
262         command.append(self._start_gre_command)
263         command.append(")")
264
265         command = " ".join(command)
266         command = self.replace_paths(command)
267
268         return command
269
270     @property
271     def _start_command(self):
272         command = []
273         command.append("sudo -S ip tuntap add %s mode tap" % self.get("deviceName"))
274         command.append("sudo -S ip link set %s up" % self.get("deviceName"))
275         command.append("sudo -S ip addr add %s/%d dev %s" % (
276             self.get("ip4"),
277             self.get("prefix4"),
278             self.get("deviceName"),
279             ))
280         return ";".join(command)
281
282     @property
283     def _stop_command(self):
284         command = []
285         command.append("sudo -S ip link set %s down" % self.get("deviceName"))
286         command.append("sudo -S ip link del %s" % self.get("deviceName"))
287         
288         return ";".join(command)
289
290     @property
291     def _start_gre_command(self):
292         command = []
293         command.append("sudo -S modprobe ip_gre")
294         command.append("sudo -S ip link add %s type gre remote %s local %s ttl 64 csum key %s" % (
295                 self.get("deviceName"),
296                 self.get("greRemote"),
297                 socket.gethostbyname(self.node.get("hostname")),
298                 self.get("greKey")
299             ))
300         command.append("sudo -S addr add dev %s %s/%d peer %s/%d" % (
301                 self.get("deviceName"),
302                 self.get("ip4"),
303                 self.get("prefix4"),
304                 self.get("pointopoint"),
305                 self.get("prefix4"),
306                 ))
307         command.append("sudo -S ip link set %s up " % self.get("deviceName"))
308
309         return ";".join(command)
310
311     @property
312     def vif_type(self):
313         return "IFF_TAP"
314
315     @property
316     def vif_prefix(self):
317         return "tap"
318
319     def sock_name(self):
320         return os.path.join(self.run_home, "tap.sock")
321
322     def valid_connection(self, guid):
323         # TODO: Validate!
324         return True
325