Lots of cross-connection fixes, TUN synchronization, etc
[nepi.git] / src / nepi / testbeds / planetlab / interfaces.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 from constants import TESTBED_ID
5 import nepi.util.ipaddr2 as ipaddr2
6 import nepi.util.server as server
7 import plcapi
8 import subprocess
9 import os
10 import os.path
11 import random
12
13 import tunproto
14
15 class NodeIface(object):
16     def __init__(self, api=None):
17         if not api:
18             api = plcapi.PLCAPI()
19         self._api = api
20         
21         # Attributes
22         self.primary = True
23
24         # These get initialized at configuration time
25         self.address = None
26         self.lladdr = None
27         self.netprefix = None
28         self.netmask = None
29         self.broadcast = True
30         self._interface_id = None
31
32         # These get initialized when the iface is connected to its node
33         self.node = None
34
35         # These get initialized when the iface is connected to the internet
36         self.has_internet = False
37
38     def __str__(self):
39         return "%s<ip:%s/%s up mac:%s>" % (
40             self.__class__.__name__,
41             self.address, self.netmask,
42             self.lladdr,
43         )
44
45     def add_address(self, address, netprefix, broadcast):
46         raise RuntimeError, "Cannot add explicit addresses to public interface"
47     
48     def pick_iface(self, siblings):
49         """
50         Picks an interface using the PLCAPI to query information about the node.
51         
52         Needs an assigned node.
53         
54         Params:
55             siblings: other NodeIface elements attached to the same node
56         """
57         
58         if self.node is None or self.node._node_id is None:
59             raise RuntimeError, "Cannot pick interface without an assigned node"
60         
61         avail = self._api.GetInterfaces(
62             node_id=self.node._node_id, 
63             is_primary=self.primary,
64             fields=('interface_id','mac','netmask','ip') )
65         
66         used = set([sibling._interface_id for sibling in siblings
67                     if sibling._interface_id is not None])
68         
69         for candidate in avail:
70             candidate_id = candidate['interface_id']
71             if candidate_id not in used:
72                 # pick it!
73                 self._interface_id = candidate_id
74                 self.address = candidate['ip']
75                 self.lladdr = candidate['mac']
76                 self.netprefix = candidate['netmask']
77                 self.netmask = ipaddr2.ipv4_dot2mask(self.netprefix) if self.netprefix else None
78                 return
79         else:
80             raise RuntimeError, "Cannot configure interface: cannot find suitable interface in PlanetLab node"
81
82     def validate(self):
83         if not self.has_internet:
84             raise RuntimeError, "All external interface devices must be connected to the Internet"
85     
86
87 class _CrossIface(object):
88     def __init__(self, proto, addr, port):
89         self.tun_proto = proto
90         self.tun_addr = addr
91         self.tun_port = port
92         
93         # Cannot access cross peers
94         self.peer_proto_impl = None
95
96 class TunIface(object):
97     _PROTO_MAP = tunproto.TUN_PROTO_MAP
98     _KIND = 'TUN'
99
100     def __init__(self, api=None):
101         if not api:
102             api = plcapi.PLCAPI()
103         self._api = api
104         
105         # Attributes
106         self.address = None
107         self.netprefix = None
108         self.netmask = None
109         
110         self.up = None
111         self.device_name = None
112         self.mtu = None
113         self.snat = False
114         self.txqueuelen = None
115         
116         # Enabled traces
117         self.capture = False
118
119         # These get initialized when the iface is connected to its node
120         self.node = None
121         
122         # These get initialized when the iface is configured
123         self.external_iface = None
124         
125         # These get initialized when the iface is configured
126         # They're part of the TUN standard attribute set
127         self.tun_port = None
128         self.tun_addr = None
129         
130         # These get initialized when the iface is connected to its peer
131         self.peer_iface = None
132         self.peer_proto = None
133         self.peer_addr = None
134         self.peer_port = None
135         self.peer_proto_impl = None
136
137         # same as peer proto, but for execute-time standard attribute lookups
138         self.tun_proto = None 
139         
140         
141         # Generate an initial random cryptographic key to use for tunnelling
142         # Upon connection, both endpoints will agree on a common one based on
143         # this one.
144         self.tun_key = ( ''.join(map(chr, [ 
145                     r.getrandbits(8) 
146                     for i in xrange(32) 
147                     for r in (random.SystemRandom(),) ])
148                 ).encode("base64").strip() )        
149         
150
151     def __str__(self):
152         return "%s<ip:%s/%s %s%s>" % (
153             self.__class__.__name__,
154             self.address, self.netprefix,
155             " up" if self.up else " down",
156             " snat" if self.snat else "",
157         )
158
159     def add_address(self, address, netprefix, broadcast):
160         if (self.address or self.netprefix or self.netmask) is not None:
161             raise RuntimeError, "Cannot add more than one address to %s interfaces" % (self._KIND,)
162         if broadcast:
163             raise ValueError, "%s interfaces cannot broadcast in PlanetLab" % (self._KIND,)
164         
165         self.address = address
166         self.netprefix = netprefix
167         self.netmask = ipaddr2.ipv4_mask2dot(netprefix)
168     
169     def validate(self):
170         if not self.node:
171             raise RuntimeError, "Unconnected %s iface - missing node" % (self._KIND,)
172         if self.peer_iface and self.peer_proto not in self._PROTO_MAP:
173             raise RuntimeError, "Unsupported tunnelling protocol: %s" % (self.peer_proto,)
174         if not self.address or not self.netprefix or not self.netmask:
175             raise RuntimeError, "Misconfigured %s iface - missing address" % (self._KIND,)
176     
177     def _impl_instance(self, home_path, listening):
178         impl = self._PROTO_MAP[self.peer_proto](
179             self, self.peer_iface, home_path, self.tun_key, listening)
180         impl.port = self.tun_port
181         return impl
182     
183     def prepare(self, home_path, listening):
184         if not self.peer_iface and (self.peer_proto and (listening or (self.peer_addr and self.peer_port))):
185             # Ad-hoc peer_iface
186             self.peer_iface = _CrossIface(
187                 self.peer_proto,
188                 self.peer_addr,
189                 self.peer_port)
190         if self.peer_iface:
191             if not self.peer_proto_impl:
192                 self.peer_proto_impl = self._impl_instance(home_path, listening)
193             self.peer_proto_impl.prepare()
194     
195     def setup(self):
196         if self.peer_proto_impl:
197             self.peer_proto_impl.setup()
198     
199     def cleanup(self):
200         if self.peer_proto_impl:
201             self.peer_proto_impl.shutdown()
202             self.peer_proto_impl = None
203
204     def async_launch_wait(self):
205         if self.peer_proto_impl:
206             self.peer_proto_impl.async_launch_wait()
207
208     def sync_trace(self, local_dir, whichtrace):
209         if self.peer_proto_impl:
210             return self.peer_proto_impl.sync_trace(local_dir, whichtrace)
211         else:
212             return None
213
214 class TapIface(TunIface):
215     _PROTO_MAP = tunproto.TAP_PROTO_MAP
216     _KIND = 'TAP'
217
218 # Yep, it does nothing - yet
219 class Internet(object):
220     def __init__(self, api=None):
221         if not api:
222             api = plcapi.PLCAPI()
223         self._api = api
224
225 class NetPipe(object):
226     def __init__(self, api=None):
227         if not api:
228             api = plcapi.PLCAPI()
229         self._api = api
230
231         # Attributes
232         self.mode = None
233         self.addrList = None
234         self.portList = None
235         
236         self.plrIn = None
237         self.bwIn = None
238         self.delayIn = None
239
240         self.plrOut = None
241         self.bwOut = None
242         self.delayOut = None
243         
244         # These get initialized when the pipe is connected to its node
245         self.node = None
246         self.configured = False
247     
248     def validate(self):
249         if not self.mode:
250             raise RuntimeError, "Undefined NetPipe mode"
251         if not self.portList:
252             raise RuntimeError, "Undefined NetPipe port list - must always define the scope"
253         if not (self.plrIn or self.bwIn or self.delayIn):
254             raise RuntimeError, "Undefined NetPipe inbound characteristics"
255         if not (self.plrOut or self.bwOut or self.delayOut):
256             raise RuntimeError, "Undefined NetPipe outbound characteristics"
257         if not self.node:
258             raise RuntimeError, "Unconnected NetPipe"
259     
260     def _add_pipedef(self, bw, plr, delay, options):
261         if delay:
262             options.extend(("delay","%dms" % (delay,)))
263         if bw:
264             options.extend(("bw","%.8fMbit/s" % (bw,)))
265         if plr:
266             options.extend(("plr","%.8f" % (plr,)))
267     
268     def _get_ruledef(self):
269         scope = "%s%s%s" % (
270             self.portList,
271             "@" if self.addrList else "",
272             self.addrList or "",
273         )
274         
275         options = []
276         if self.bwIn or self.plrIn or self.delayIn:
277             options.append("IN")
278             self._add_pipedef(self.bwIn, self.plrIn, self.delayIn, options)
279         if self.bwOut or self.plrOut or self.delayOut:
280             options.append("OUT")
281             self._add_pipedef(self.bwOut, self.plrOut, self.delayOut, options)
282         options = ' '.join(options)
283         
284         return (scope,options)
285
286     def configure(self):
287         # set up rule
288         scope, options = self._get_ruledef()
289         command = "sudo -S netconfig config %s %s %s" % (self.mode, scope, options)
290         
291         (out,err),proc = server.popen_ssh_command(
292             command,
293             host = self.node.hostname,
294             port = None,
295             user = self.node.slicename,
296             agent = None,
297             ident_key = self.node.ident_path,
298             server_key = self.node.server_key
299             )
300     
301         if proc.wait():
302             raise RuntimeError, "Failed instal build sources: %s %s" % (out,err,)
303         
304         # we have to clean up afterwards
305         self.configured = True
306     
307     def refresh(self):
308         if self.configured:
309             # refresh rule
310             scope, options = self._get_ruledef()
311             command = "sudo -S netconfig refresh %s %s %s" % (self.mode, scope, options)
312             
313             (out,err),proc = server.popen_ssh_command(
314                 command,
315                 host = self.node.hostname,
316                 port = None,
317                 user = self.node.slicename,
318                 agent = None,
319                 ident_key = self.node.ident_path,
320                 server_key = self.node.server_key
321                 )
322         
323             if proc.wait():
324                 raise RuntimeError, "Failed instal build sources: %s %s" % (out,err,)
325     
326     def cleanup(self):
327         if self.configured:
328             # remove rule
329             scope, options = self._get_ruledef()
330             command = "sudo -S netconfig delete %s %s" % (self.mode, scope)
331             
332             (out,err),proc = server.popen_ssh_command(
333                 command,
334                 host = self.node.hostname,
335                 port = None,
336                 user = self.node.slicename,
337                 agent = None,
338                 ident_key = self.node.ident_path,
339                 server_key = self.node.server_key
340                 )
341         
342             if proc.wait():
343                 raise RuntimeError, "Failed instal build sources: %s %s" % (out,err,)
344             
345             self.configured = False
346     
347     def sync_trace(self, local_dir, whichtrace):
348         if whichtrace != 'netpipeStats':
349             raise ValueError, "Unsupported trace %s" % (whichtrace,)
350         
351         local_path = os.path.join(local_dir, "netpipe_stats_%s" % (self.mode,))
352         
353         # create parent local folders
354         proc = subprocess.Popen(
355             ["mkdir", "-p", os.path.dirname(local_path)],
356             stdout = open("/dev/null","w"),
357             stdin = open("/dev/null","r"))
358
359         if proc.wait():
360             raise RuntimeError, "Failed to synchronize trace: %s %s" % (out,err,)
361         
362         (out,err),proc = server.popen_ssh_command(
363             "echo 'Rules:' ; sudo -S netconfig show rules ; echo 'Pipes:' ; sudo -S netconfig show pipes",
364             host = self.node.hostname,
365             port = None,
366             user = self.node.slicename,
367             agent = None,
368             ident_key = self.node.ident_path,
369             server_key = self.node.server_key
370             )
371         
372         if proc.wait():
373             raise RuntimeError, "Failed to synchronize trace: %s %s" % (out,err,)
374         
375         # dump results to file
376         f = open(local_path, "wb")
377         f.write(err or "")
378         f.write(out or "")
379         f.close()
380         
381         return local_path
382