PL Point-to-point link support (TUNs automatically set their interfaces for P2P,...
[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     def __str__(self):
97         return "%s%r" % (
98             self.__class__.__name__,
99             ( self.tun_proto,
100               self.tun_addr,
101               self.tun_port ) 
102         )
103
104 class TunIface(object):
105     _PROTO_MAP = tunproto.TUN_PROTO_MAP
106     _KIND = 'TUN'
107
108     def __init__(self, api=None):
109         if not api:
110             api = plcapi.PLCAPI()
111         self._api = api
112         
113         # Attributes
114         self.address = None
115         self.netprefix = None
116         self.netmask = None
117         
118         self.up = None
119         self.device_name = None
120         self.mtu = None
121         self.snat = False
122         self.txqueuelen = None
123         self.pointopoint = None
124         
125         # Enabled traces
126         self.capture = False
127
128         # These get initialized when the iface is connected to its node
129         self.node = None
130         
131         # These get initialized when the iface is configured
132         self.external_iface = None
133         
134         # These get initialized when the iface is configured
135         # They're part of the TUN standard attribute set
136         self.tun_port = None
137         self.tun_addr = None
138         
139         # These get initialized when the iface is connected to its peer
140         self.peer_iface = None
141         self.peer_proto = None
142         self.peer_addr = None
143         self.peer_port = None
144         self.peer_proto_impl = None
145
146         # same as peer proto, but for execute-time standard attribute lookups
147         self.tun_proto = None 
148         
149         
150         # Generate an initial random cryptographic key to use for tunnelling
151         # Upon connection, both endpoints will agree on a common one based on
152         # this one.
153         self.tun_key = ( ''.join(map(chr, [ 
154                     r.getrandbits(8) 
155                     for i in xrange(32) 
156                     for r in (random.SystemRandom(),) ])
157                 ).encode("base64").strip() )        
158         
159
160     def __str__(self):
161         return "%s<ip:%s/%s %s%s>" % (
162             self.__class__.__name__,
163             self.address, self.netprefix,
164             " up" if self.up else " down",
165             " snat" if self.snat else "",
166         )
167
168     def add_address(self, address, netprefix, broadcast):
169         if (self.address or self.netprefix or self.netmask) is not None:
170             raise RuntimeError, "Cannot add more than one address to %s interfaces" % (self._KIND,)
171         if broadcast:
172             raise ValueError, "%s interfaces cannot broadcast in PlanetLab" % (self._KIND,)
173         
174         self.address = address
175         self.netprefix = netprefix
176         self.netmask = ipaddr2.ipv4_mask2dot(netprefix)
177     
178     def validate(self):
179         if not self.node:
180             raise RuntimeError, "Unconnected %s iface - missing node" % (self._KIND,)
181         if self.peer_iface and self.peer_proto not in self._PROTO_MAP:
182             raise RuntimeError, "Unsupported tunnelling protocol: %s" % (self.peer_proto,)
183         if not self.address or not self.netprefix or not self.netmask:
184             raise RuntimeError, "Misconfigured %s iface - missing address" % (self._KIND,)
185     
186     def _impl_instance(self, home_path, listening):
187         impl = self._PROTO_MAP[self.peer_proto](
188             self, self.peer_iface, home_path, self.tun_key, listening)
189         impl.port = self.tun_port
190         return impl
191     
192     def prepare(self, home_path, listening):
193         if not self.peer_iface and (self.peer_proto and (listening or (self.peer_addr and self.peer_port))):
194             # Ad-hoc peer_iface
195             self.peer_iface = _CrossIface(
196                 self.peer_proto,
197                 self.peer_addr,
198                 self.peer_port)
199         if self.peer_iface:
200             if not self.peer_proto_impl:
201                 self.peer_proto_impl = self._impl_instance(home_path, listening)
202             self.peer_proto_impl.prepare()
203     
204     def setup(self):
205         if self.peer_proto_impl:
206             self.peer_proto_impl.setup()
207     
208     def cleanup(self):
209         if self.peer_proto_impl:
210             self.peer_proto_impl.shutdown()
211             self.peer_proto_impl = None
212
213     def async_launch_wait(self):
214         if self.peer_proto_impl:
215             self.peer_proto_impl.async_launch_wait()
216
217     def sync_trace(self, local_dir, whichtrace):
218         if self.peer_proto_impl:
219             return self.peer_proto_impl.sync_trace(local_dir, whichtrace)
220         else:
221             return None
222
223 class TapIface(TunIface):
224     _PROTO_MAP = tunproto.TAP_PROTO_MAP
225     _KIND = 'TAP'
226
227 # Yep, it does nothing - yet
228 class Internet(object):
229     def __init__(self, api=None):
230         if not api:
231             api = plcapi.PLCAPI()
232         self._api = api
233
234 class NetPipe(object):
235     def __init__(self, api=None):
236         if not api:
237             api = plcapi.PLCAPI()
238         self._api = api
239
240         # Attributes
241         self.mode = None
242         self.addrList = None
243         self.portList = None
244         
245         self.plrIn = None
246         self.bwIn = None
247         self.delayIn = None
248
249         self.plrOut = None
250         self.bwOut = None
251         self.delayOut = None
252         
253         # These get initialized when the pipe is connected to its node
254         self.node = None
255         self.configured = False
256     
257     def validate(self):
258         if not self.mode:
259             raise RuntimeError, "Undefined NetPipe mode"
260         if not self.portList:
261             raise RuntimeError, "Undefined NetPipe port list - must always define the scope"
262         if not (self.plrIn or self.bwIn or self.delayIn):
263             raise RuntimeError, "Undefined NetPipe inbound characteristics"
264         if not (self.plrOut or self.bwOut or self.delayOut):
265             raise RuntimeError, "Undefined NetPipe outbound characteristics"
266         if not self.node:
267             raise RuntimeError, "Unconnected NetPipe"
268     
269     def _add_pipedef(self, bw, plr, delay, options):
270         if delay:
271             options.extend(("delay","%dms" % (delay,)))
272         if bw:
273             options.extend(("bw","%.8fMbit/s" % (bw,)))
274         if plr:
275             options.extend(("plr","%.8f" % (plr,)))
276     
277     def _get_ruledef(self):
278         scope = "%s%s%s" % (
279             self.portList,
280             "@" if self.addrList else "",
281             self.addrList or "",
282         )
283         
284         options = []
285         if self.bwIn or self.plrIn or self.delayIn:
286             options.append("IN")
287             self._add_pipedef(self.bwIn, self.plrIn, self.delayIn, options)
288         if self.bwOut or self.plrOut or self.delayOut:
289             options.append("OUT")
290             self._add_pipedef(self.bwOut, self.plrOut, self.delayOut, options)
291         options = ' '.join(options)
292         
293         return (scope,options)
294
295     def configure(self):
296         # set up rule
297         scope, options = self._get_ruledef()
298         command = "sudo -S netconfig config %s %s %s" % (self.mode, scope, options)
299         
300         (out,err),proc = server.popen_ssh_command(
301             command,
302             host = self.node.hostname,
303             port = None,
304             user = self.node.slicename,
305             agent = None,
306             ident_key = self.node.ident_path,
307             server_key = self.node.server_key
308             )
309     
310         if proc.wait():
311             raise RuntimeError, "Failed instal build sources: %s %s" % (out,err,)
312         
313         # we have to clean up afterwards
314         self.configured = True
315     
316     def refresh(self):
317         if self.configured:
318             # refresh rule
319             scope, options = self._get_ruledef()
320             command = "sudo -S netconfig refresh %s %s %s" % (self.mode, scope, options)
321             
322             (out,err),proc = server.popen_ssh_command(
323                 command,
324                 host = self.node.hostname,
325                 port = None,
326                 user = self.node.slicename,
327                 agent = None,
328                 ident_key = self.node.ident_path,
329                 server_key = self.node.server_key
330                 )
331         
332             if proc.wait():
333                 raise RuntimeError, "Failed instal build sources: %s %s" % (out,err,)
334     
335     def cleanup(self):
336         if self.configured:
337             # remove rule
338             scope, options = self._get_ruledef()
339             command = "sudo -S netconfig delete %s %s" % (self.mode, scope)
340             
341             (out,err),proc = server.popen_ssh_command(
342                 command,
343                 host = self.node.hostname,
344                 port = None,
345                 user = self.node.slicename,
346                 agent = None,
347                 ident_key = self.node.ident_path,
348                 server_key = self.node.server_key
349                 )
350         
351             if proc.wait():
352                 raise RuntimeError, "Failed instal build sources: %s %s" % (out,err,)
353             
354             self.configured = False
355     
356     def sync_trace(self, local_dir, whichtrace):
357         if whichtrace != 'netpipeStats':
358             raise ValueError, "Unsupported trace %s" % (whichtrace,)
359         
360         local_path = os.path.join(local_dir, "netpipe_stats_%s" % (self.mode,))
361         
362         # create parent local folders
363         proc = subprocess.Popen(
364             ["mkdir", "-p", os.path.dirname(local_path)],
365             stdout = open("/dev/null","w"),
366             stdin = open("/dev/null","r"))
367
368         if proc.wait():
369             raise RuntimeError, "Failed to synchronize trace: %s %s" % (out,err,)
370         
371         (out,err),proc = server.popen_ssh_command(
372             "echo 'Rules:' ; sudo -S netconfig show rules ; echo 'Pipes:' ; sudo -S netconfig show pipes",
373             host = self.node.hostname,
374             port = None,
375             user = self.node.slicename,
376             agent = None,
377             ident_key = self.node.ident_path,
378             server_key = self.node.server_key
379             )
380         
381         if proc.wait():
382             raise RuntimeError, "Failed to synchronize trace: %s %s" % (out,err,)
383         
384         # dump results to file
385         f = open(local_path, "wb")
386         f.write(err or "")
387         f.write(out or "")
388         f.close()
389         
390         return local_path
391