Ticket #14: WIP, only intra-PL TUN connections, required groundwork for cross-backend...
[nepi.git] / src / nepi / testbeds / planetlab / tunproto.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 import weakref
5 import os
6 import os.path
7 import rspawn
8 import subprocess
9
10 from nepi.util import server
11
12 class TunProtoBase(object):
13     def __init__(self, local, peer, home_path):
14         # Weak references, since ifaces do have a reference to the
15         # tunneling protocol implementation - we don't want strong
16         # circular references.
17         self.peer = weakref.ref(peer)
18         self.local = weakref.ref(local)
19         
20         self.port = 15000
21         self.mode = 'pl-tun'
22         
23         self.home_path = home_path
24         
25         self._started = False
26         self._pid = None
27         self._ppid = None
28
29     def _make_home(self):
30         local = self.local()
31         
32         if not local:
33             raise RuntimeError, "Lost reference to peering interfaces before launching"
34         if not local.node:
35             raise RuntimeError, "Unconnected TUN - missing node"
36         
37         # Make sure all the paths are created where 
38         # they have to be created for deployment
39         cmd = "mkdir -p %s" % (server.shell_escape(self.home_path),)
40         (out,err),proc = server.popen_ssh_command(
41             cmd,
42             host = local.node.hostname,
43             port = None,
44             user = local.node.slicename,
45             agent = None,
46             ident_key = local.node.ident_path,
47             server_key = local.node.server_key
48             )
49         
50         if proc.wait():
51             raise RuntimeError, "Failed to set up TUN forwarder: %s %s" % (out,err,)
52         
53     
54     def _install_scripts(self):
55         local = self.local()
56         
57         if not local:
58             raise RuntimeError, "Lost reference to peering interfaces before launching"
59         if not local.node:
60             raise RuntimeError, "Unconnected TUN - missing node"
61         
62         # Install the tun_connect script and tunalloc utility
63         source = os.path.join(os.path.dirname(__file__), 'scripts', 'tun_connect.py')
64         dest = "%s@%s:%s" % (
65             local.node.slicename, local.node.hostname, 
66             os.path.join(self.home_path,'.'),)
67         (out,err),proc = server.popen_scp(
68             source,
69             dest,
70             ident_key = local.node.ident_path,
71             server_key = local.node.server_key
72             )
73     
74         if proc.wait():
75             raise RuntimeError, "Failed upload TUN connect script %r: %s %s" % (source, out,err,)
76
77         source = os.path.join(os.path.dirname(__file__), 'scripts', 'tunalloc.c')
78         (out,err),proc = server.popen_scp(
79             source,
80             dest,
81             ident_key = local.node.ident_path,
82             server_key = local.node.server_key
83             )
84     
85         if proc.wait():
86             raise RuntimeError, "Failed upload TUN connect script %r: %s %s" % (source, out,err,)
87
88         cmd = "cd %s && gcc -shared tunalloc.c -o tunalloc.so" % (server.shell_escape(self.home_path),)
89         (out,err),proc = server.popen_ssh_command(
90             cmd,
91             host = local.node.hostname,
92             port = None,
93             user = local.node.slicename,
94             agent = None,
95             ident_key = local.node.ident_path,
96             server_key = local.node.server_key
97             )
98         
99         if proc.wait():
100             raise RuntimeError, "Failed to set up TUN forwarder: %s %s" % (out,err,)
101         
102     
103     def launch(self, check_proto, listen, extra_args=[]):
104         peer = self.peer()
105         local = self.local()
106         
107         if not peer or not local:
108             raise RuntimeError, "Lost reference to peering interfaces before launching"
109         
110         peer_port = peer.tun_port
111         peer_addr = peer.tun_addr
112         peer_proto= peer.tun_proto
113         
114         local_port = self.port
115         local_cap  = local.capture
116         local_addr = local.address
117         local_mask = local.netprefix
118         local_snat = local.snat
119         local_txq  = local.txqueuelen
120         
121         if check_proto != peer_proto:
122             raise RuntimeError, "Peering protocol mismatch: %s != %s" % (check_proto, peer_proto)
123         
124         if not listen and (not peer_port or not peer_addr):
125             raise RuntimeError, "Misconfigured peer: %s" % (peer,)
126         
127         if listen and (not local_port or not local_addr or not local_mask):
128             raise RuntimeError, "Misconfigured TUN: %s" % (local,)
129         
130         args = ["python", "tun_connect.py", 
131             "-m", str(self.mode),
132             "-p", str(local_port if listen else peer_port),
133             "-A", str(local_addr),
134             "-M", str(local_mask)]
135         
136         if local_snat:
137             args.append("-S")
138         if local_txq:
139             args.extend(("-Q",str(local_txq)))
140         if extra_args:
141             args.extend(map(str,extra_args))
142         if not listen:
143             args.append(str(peer_addr))
144         
145         self._make_home()
146         self._install_scripts()
147         
148         # Start process in a "daemonized" way, using nohup and heavy
149         # stdin/out redirection to avoid connection issues
150         (out,err),proc = rspawn.remote_spawn(
151             " ".join(args),
152             
153             pidfile = './pid',
154             home = self.home_path,
155             stdin = '/dev/null',
156             stdout = 'capture' if local_cap else '/dev/null',
157             stderr = rspawn.STDOUT,
158             sudo = True,
159             
160             host = local.node.hostname,
161             port = None,
162             user = local.node.slicename,
163             agent = None,
164             ident_key = local.node.ident_path,
165             server_key = local.node.server_key
166             )
167         
168         if proc.wait():
169             raise RuntimeError, "Failed to set up application: %s %s" % (out,err,)
170
171         self._started = True
172
173     def checkpid(self):            
174         local = self.local()
175         
176         if not local:
177             raise RuntimeError, "Lost reference to local interface"
178         
179         # Get PID/PPID
180         # NOTE: wait a bit for the pidfile to be created
181         if self._started and not self._pid or not self._ppid:
182             pidtuple = rspawn.remote_check_pid(
183                 os.path.join(self.home_path,'pid'),
184                 host = local.node.hostname,
185                 port = None,
186                 user = local.node.slicename,
187                 agent = None,
188                 ident_key = local.node.ident_path,
189                 server_key = local.node.server_key
190                 )
191             
192             if pidtuple:
193                 self._pid, self._ppid = pidtuple
194     
195     def status(self):
196         local = self.local()
197         
198         if not local:
199             raise RuntimeError, "Lost reference to local interface"
200         
201         self.checkpid()
202         if not self._started:
203             return rspawn.NOT_STARTED
204         elif not self._pid or not self._ppid:
205             return rspawn.NOT_STARTED
206         else:
207             status = rspawn.remote_status(
208                 self._pid, self._ppid,
209                 host = local.node.hostname,
210                 port = None,
211                 user = local.node.slicename,
212                 agent = None,
213                 ident_key = local.node.ident_path
214                 )
215             return status
216     
217     def kill(self):
218         local = self.local()
219         
220         if not local:
221             raise RuntimeError, "Lost reference to local interface"
222         
223         status = self.status()
224         if status == rspawn.RUNNING:
225             # kill by ppid+pid - SIGTERM first, then try SIGKILL
226             rspawn.remote_kill(
227                 self._pid, self._ppid,
228                 host = local.node.hostname,
229                 port = None,
230                 user = local.node.slicename,
231                 agent = None,
232                 ident_key = local.node.ident_path,
233                 server_key = local.node.server_key,
234                 sudo = True
235                 )
236         
237     def sync_trace(self, local_dir, whichtrace):
238         if whichtrace != 'packets':
239             return None
240         
241         local = self.local()
242         
243         if not local:
244             return None
245         
246         local_path = os.path.join(local_dir, 'capture')
247         
248         # create parent local folders
249         proc = subprocess.Popen(
250             ["mkdir", "-p", os.path.dirname(local_path)],
251             stdout = open("/dev/null","w"),
252             stdin = open("/dev/null","r"))
253
254         if proc.wait():
255             raise RuntimeError, "Failed to synchronize trace: %s %s" % (out,err,)
256         
257         # sync files
258         (out,err),proc = server.popen_scp(
259             '%s@%s:%s' % (local.node.slicename, local.node.hostname, 
260                 os.path.join(self.home_path, 'capture')),
261             local_path,
262             port = None,
263             agent = None,
264             ident_key = local.node.ident_path,
265             server_key = local.node.server_key
266             )
267         
268         if proc.wait():
269             raise RuntimeError, "Failed to synchronize trace: %s %s" % (out,err,)
270         
271         return local_path
272         
273         
274     def prepare(self):
275         """
276         First-phase setup
277         
278         eg: set up listening ports
279         """
280         raise NotImplementedError
281     
282     def setup(self):
283         """
284         Second-phase setup
285         
286         eg: connect to peer
287         """
288         raise NotImplementedError
289     
290     def shutdown(self):
291         """
292         Cleanup
293         """
294         raise NotImplementedError
295         
296
297 class TunProtoUDP(TunProtoBase):
298     def __init__(self, local, peer, home_path, listening):
299         super(TunProtoTCP, self).__init__(local, peer, home_path)
300         self.listening = listening
301     
302     def prepare(self):
303         pass
304     
305     def setup(self):
306         self.launch('udp', False, ("-U",))
307     
308     def shutdown(self):
309         self.kill()
310
311 class TunProtoTCP(TunProtoBase):
312     def __init__(self, local, peer, home_path, listening):
313         super(TunProtoTCP, self).__init__(local, peer, home_path)
314         self.listening = listening
315     
316     def prepare(self):
317         if self.listening:
318             self.launch('tcp', True)
319     
320     def setup(self):
321         if not self.listening:
322             self.launch('tcp', False)
323         
324         self.checkpid()
325     
326     def shutdown(self):
327         self.kill()
328
329 PROTO_MAP = {
330     'tcp' : TunProtoTCP,
331     'udp' : TunProtoUDP,
332 }
333
334