fixed shebangs in non executable .py files
[nepi.git] / src / nepi / testbeds / planetlab / interfaces.py
1 # -*- coding: utf-8 -*-
2
3 from constants import TESTBED_ID
4 import nepi.util.ipaddr2 as ipaddr2
5 import nepi.util.server as server
6 import plcapi
7 import subprocess
8 import os
9 import os.path
10 import random
11 import ipaddr
12 import functools
13
14 import tunproto
15
16 class NodeIface(object):
17     def __init__(self, api=None):
18         if not api:
19             api = plcapi.PLCAPI()
20         self._api = api
21         
22         # Attributes
23         self.primary = True
24
25         # These get initialized at configuration time
26         self.address = None
27         self.lladdr = None
28         self.netprefix = None
29         self.netmask = None
30         self.broadcast = True
31         self._interface_id = None
32
33         # These get initialized when the iface is connected to its node
34         self.node = None
35
36         # These get initialized when the iface is connected to the internet
37         self.has_internet = False
38
39     def __str__(self):
40         return "%s<ip:%s/%s up mac:%s>" % (
41             self.__class__.__name__,
42             self.address, self.netmask,
43             self.lladdr,
44         )
45     
46     __repr__ = __str__
47
48     def add_address(self, address, netprefix, broadcast):
49         raise RuntimeError, "Cannot add explicit addresses to public interface"
50     
51     def pick_iface(self, siblings):
52         """
53         Picks an interface using the PLCAPI to query information about the node.
54         
55         Needs an assigned node.
56         
57         Params:
58             siblings: other NodeIface elements attached to the same node
59         """
60         
61         if self.node is None or self.node._node_id is None:
62             raise RuntimeError, "Cannot pick interface without an assigned node"
63         
64         avail = self._api.GetInterfaces(
65             node_id=self.node._node_id, 
66             is_primary=self.primary,
67             fields=('interface_id','mac','netmask','ip') )
68         
69         used = set([sibling._interface_id for sibling in siblings
70                     if sibling._interface_id is not None])
71         
72         for candidate in avail:
73             candidate_id = candidate['interface_id']
74             if candidate_id not in used:
75                 # pick it!
76                 self._interface_id = candidate_id
77                 self.address = candidate['ip']
78                 self.lladdr = candidate['mac']
79                 self.netprefix = candidate['netmask']
80                 self.netmask = ipaddr2.ipv4_dot2mask(self.netprefix) if self.netprefix else None
81                 return
82         else:
83             raise RuntimeError, "Cannot configure interface: cannot find suitable interface in PlanetLab node"
84
85     def validate(self):
86         if not self.has_internet:
87             raise RuntimeError, "All external interface devices must be connected to the Internet"
88     
89
90 class _CrossIface(object):
91     def __init__(self, proto, addr, port, cipher):
92         self.tun_proto = proto
93         self.tun_addr = addr
94         self.tun_port = port
95         self.tun_cipher = cipher
96
97         # Attributes
98         self.address = None
99         self.netprefix = None
100         self.netmask = None
101  
102         # Cannot access cross peers
103         self.peer_proto_impl = None
104     
105     def __str__(self):
106         return "%s%r" % (
107             self.__class__.__name__,
108             ( self.tun_proto,
109               self.tun_addr,
110               self.tun_port,
111               self.tun_cipher ) 
112         )
113     
114     __repr__ = __str__
115
116 class TunIface(object):
117     _PROTO_MAP = tunproto.TUN_PROTO_MAP
118     _KIND = 'TUN'
119
120     def __init__(self, api=None):
121         if not api:
122             api = plcapi.PLCAPI()
123         self._api = api
124         
125         # Attributes
126         self.address = None
127         self.netprefix = None
128         self.netmask = None
129         
130         self.up = None
131         self.mtu = None
132         self.snat = False
133         self.txqueuelen = 1000
134         self.pointopoint = None
135         self.multicast = False
136         self.bwlimit = None
137         
138         # Enabled traces
139         self.capture = False
140
141         # These get initialized when the iface is connected to its node
142         self.node = None
143         
144         # These get initialized when the iface is connected to any filter
145         self.filter_module = None
146         self.multicast_forwarder = None
147         
148         # These get initialized when the iface is configured
149         self.external_iface = None
150         
151         # These get initialized when the iface is configured
152         # They're part of the TUN standard attribute set
153         self.tun_port = None
154         self.tun_addr = None
155         self.tun_cipher = "AES"
156         
157         # These get initialized when the iface is connected to its peer
158         self.peer_iface = None
159         self.peer_proto = None
160         self.peer_addr = None
161         self.peer_port = None
162         self.peer_proto_impl = None
163         self._delay_recover = False
164
165         # same as peer proto, but for execute-time standard attribute lookups
166         self.tun_proto = None 
167         
168         
169         # Generate an initial random cryptographic key to use for tunnelling
170         # Upon connection, both endpoints will agree on a common one based on
171         # this one.
172         self.tun_key = ( ''.join(map(chr, [ 
173                     r.getrandbits(8) 
174                     for i in xrange(32) 
175                     for r in (random.SystemRandom(),) ])
176                 ).encode("base64").strip() )        
177         
178
179     def __str__(self):
180         return "%s<ip:%s/%s %s%s%s>" % (
181             self.__class__.__name__,
182             self.address, self.netprefix,
183             " up" if self.up else " down",
184             " snat" if self.snat else "",
185             (" p2p %s" % (self.pointopoint,)) if self.pointopoint else "",
186         )
187     
188     __repr__ = __str__
189     
190     @property
191     def if_name(self):
192         if self.peer_proto_impl:
193             return self.peer_proto_impl.if_name
194
195     def routes_here(self, route):
196         """
197         Returns True if the route should be attached to this interface
198         (ie, it references a gateway in this interface's network segment)
199         """
200         if self.address and self.netprefix:
201             addr, prefix = self.address, self.netprefix
202             pointopoint = self.pointopoint
203             if not pointopoint and self.peer_iface:
204                 pointopoint = self.peer_iface.address
205             
206             if pointopoint:
207                 prefix = 32
208                 
209             dest, destprefix, nexthop, metric = route
210             
211             myNet = ipaddr.IPNetwork("%s/%d" % (addr, prefix))
212             gwIp = ipaddr.IPNetwork(nexthop)
213             
214             if pointopoint:
215                 peerIp = ipaddr.IPNetwork(pointopoint)
216                 
217                 if gwIp == peerIp:
218                     return True
219             else:
220                 if gwIp in myNet:
221                     return True
222         return False
223     
224     def add_address(self, address, netprefix, broadcast):
225         if (self.address or self.netprefix or self.netmask) is not None:
226             raise RuntimeError, "Cannot add more than one address to %s interfaces" % (self._KIND,)
227         if broadcast:
228             raise ValueError, "%s interfaces cannot broadcast in PlanetLab (%s)" % (self._KIND,broadcast)
229         
230         self.address = address
231         self.netprefix = netprefix
232         self.netmask = ipaddr2.ipv4_mask2dot(netprefix)
233     
234     def validate(self):
235         if not self.node:
236             raise RuntimeError, "Unconnected %s iface - missing node" % (self._KIND,)
237         if self.peer_iface and self.peer_proto not in self._PROTO_MAP:
238             raise RuntimeError, "Unsupported tunnelling protocol: %s" % (self.peer_proto,)
239         if not self.address or not self.netprefix or not self.netmask:
240             raise RuntimeError, "Misconfigured %s iface - missing address" % (self._KIND,)
241         if self.filter_module and self.peer_proto not in ('udp','tcp',None):
242             raise RuntimeError, "Miscofnigured TUN: %s - filtered tunnels only work with udp or tcp links" % (self,)
243         if self.tun_cipher != 'PLAIN' and self.peer_proto not in ('udp','tcp',None):
244             raise RuntimeError, "Miscofnigured TUN: %s - ciphered tunnels only work with udp or tcp links" % (self,)
245     
246     def _impl_instance(self, home_path):
247         impl = self._PROTO_MAP[self.peer_proto](
248             self, self.peer_iface, home_path, self.tun_key)
249         impl.port = self.tun_port
250         impl.cross_slice = not self.peer_iface or isinstance(self.peer_iface, _CrossIface)
251         return impl
252     
253     def recover(self):
254         if self.peer_proto:
255             self.peer_proto_impl = self._impl_instance(
256                 self._home_path)
257             self.peer_proto_impl.recover()
258         else:
259             self._delay_recover = True
260     
261     def prepare(self, home_path):
262         if not self.peer_iface and (self.peer_proto and self.peer_addr):
263             # Ad-hoc peer_iface
264             self.peer_iface = _CrossIface(
265                 self.peer_proto,
266                 self.peer_addr,
267                 self.peer_port,
268                 self.peer_cipher)
269         if self.peer_iface:
270             if not self.peer_proto_impl:
271                 self.peer_proto_impl = self._impl_instance(home_path)
272             if self._delay_recover:
273                 self.peer_proto_impl.recover()
274     
275     def launch(self):
276         if self.peer_proto_impl:
277             self.peer_proto_impl.launch()
278     
279     def cleanup(self):
280         if self.peer_proto_impl:
281             self.peer_proto_impl.shutdown()
282
283     def destroy(self):
284         if self.peer_proto_impl:
285             self.peer_proto_impl.destroy()
286             self.peer_proto_impl = None
287
288     def wait(self):
289         if self.peer_proto_impl:
290             self.peer_proto_impl.wait()
291
292     def sync_trace(self, local_dir, whichtrace, tracemap = None):
293         if self.peer_proto_impl:
294             return self.peer_proto_impl.sync_trace(local_dir, whichtrace,
295                     tracemap)
296         else:
297             return None
298
299     def remote_trace_path(self, whichtrace, tracemap = None):
300         if self.peer_proto_impl:
301             return self.peer_proto_impl.remote_trace_path(whichtrace, tracemap)
302         else:
303             return None
304
305     def remote_trace_name(self, whichtrace):
306         return whichtrace
307
308 class TapIface(TunIface):
309     _PROTO_MAP = tunproto.TAP_PROTO_MAP
310     _KIND = 'TAP'
311
312 # Yep, it does nothing - yet
313 class Internet(object):
314     def __init__(self, api=None):
315         if not api:
316             api = plcapi.PLCAPI()
317         self._api = api
318
319 class NetPipe(object):
320     def __init__(self, api=None):
321         if not api:
322             api = plcapi.PLCAPI()
323         self._api = api
324
325         # Attributes
326         self.mode = None
327         self.addrList = None
328         self.portList = None
329         
330         self.plrIn = None
331         self.bwIn = None
332         self.delayIn = None
333
334         self.plrOut = None
335         self.bwOut = None
336         self.delayOut = None
337         
338         # These get initialized when the pipe is connected to its node
339         self.node = None
340         self.configured = False
341     
342     def validate(self):
343         if not self.mode:
344             raise RuntimeError, "Undefined NetPipe mode"
345         if not self.portList:
346             raise RuntimeError, "Undefined NetPipe port list - must always define the scope"
347         if not (self.plrIn or self.bwIn or self.delayIn):
348             raise RuntimeError, "Undefined NetPipe inbound characteristics"
349         if not (self.plrOut or self.bwOut or self.delayOut):
350             raise RuntimeError, "Undefined NetPipe outbound characteristics"
351         if not self.node:
352             raise RuntimeError, "Unconnected NetPipe"
353     
354     def _add_pipedef(self, bw, plr, delay, options):
355         if delay:
356             options.extend(("delay","%dms" % (delay,)))
357         if bw:
358             options.extend(("bw","%.8fMbit/s" % (bw,)))
359         if plr:
360             options.extend(("plr","%.8f" % (plr,)))
361     
362     def _get_ruledef(self):
363         scope = "%s%s%s" % (
364             self.portList,
365             "@" if self.addrList else "",
366             self.addrList or "",
367         )
368         
369         options = []
370         if self.bwIn or self.plrIn or self.delayIn:
371             options.append("IN")
372             self._add_pipedef(self.bwIn, self.plrIn, self.delayIn, options)
373         if self.bwOut or self.plrOut or self.delayOut:
374             options.append("OUT")
375             self._add_pipedef(self.bwOut, self.plrOut, self.delayOut, options)
376         options = ' '.join(options)
377         
378         return (scope,options)
379     
380     def recover(self):
381         # Rules are safe on their nodes
382         self.configured = True
383
384     def configure(self):
385         # set up rule
386         scope, options = self._get_ruledef()
387         command = "sudo -S netconfig config %s %s %s" % (self.mode, scope, options)
388         
389         (out,err),proc = server.popen_ssh_command(
390             command,
391             host = self.node.hostname,
392             port = None,
393             user = self.node.slicename,
394             agent = None,
395             ident_key = self.node.ident_path,
396             server_key = self.node.server_key
397             )
398     
399         if proc.wait():
400             raise RuntimeError, "Failed instal build sources: %s %s" % (out,err,)
401         
402         # we have to clean up afterwards
403         self.configured = True
404     
405     def refresh(self):
406         if self.configured:
407             # refresh rule
408             scope, options = self._get_ruledef()
409             command = "sudo -S netconfig refresh %s %s %s" % (self.mode, scope, options)
410             
411             (out,err),proc = server.popen_ssh_command(
412                 command,
413                 host = self.node.hostname,
414                 port = None,
415                 user = self.node.slicename,
416                 agent = None,
417                 ident_key = self.node.ident_path,
418                 server_key = self.node.server_key
419                 )
420         
421             if proc.wait():
422                 raise RuntimeError, "Failed instal build sources: %s %s" % (out,err,)
423     
424     def cleanup(self):
425         if self.configured:
426             # remove rule
427             scope, options = self._get_ruledef()
428             command = "sudo -S netconfig delete %s %s" % (self.mode, scope)
429             
430             (out,err),proc = server.popen_ssh_command(
431                 command,
432                 host = self.node.hostname,
433                 port = None,
434                 user = self.node.slicename,
435                 agent = None,
436                 ident_key = self.node.ident_path,
437                 server_key = self.node.server_key
438                 )
439         
440             if proc.wait():
441                 raise RuntimeError, "Failed instal build sources: %s %s" % (out,err,)
442             
443             self.configured = False
444     
445     def sync_trace(self, local_dir, whichtrace):
446         if whichtrace != 'netpipeStats':
447             raise ValueError, "Unsupported trace %s" % (whichtrace,)
448         
449         local_path = os.path.join(local_dir, "netpipe_stats_%s" % (self.mode,))
450         
451         # create parent local folders
452         proc = subprocess.Popen(
453             ["mkdir", "-p", os.path.dirname(local_path)],
454             stdout = open("/dev/null","w"),
455             stdin = open("/dev/null","r"))
456
457         if proc.wait():
458             raise RuntimeError, "Failed to synchronize trace: %s %s" % (out,err,)
459         
460         (out,err),proc = server.popen_ssh_command(
461             "echo 'Rules:' ; sudo -S netconfig show rules ; echo 'Pipes:' ; sudo -S netconfig show pipes",
462             host = self.node.hostname,
463             port = None,
464             user = self.node.slicename,
465             agent = None,
466             ident_key = self.node.ident_path,
467             server_key = self.node.server_key
468             )
469         
470         if proc.wait():
471             raise RuntimeError, "Failed to synchronize trace: %s %s" % (out,err,)
472         
473         # dump results to file
474         f = open(local_path, "wb")
475         f.write(err or "")
476         f.write(out or "")
477         f.close()
478         
479         return local_path
480     
481 class TunFilter(object):
482     _TRACEMAP = {
483         # tracename : (remotename, localname)
484     }
485     
486     def __init__(self, api=None):
487         if not api:
488             api = plcapi.PLCAPI()
489         self._api = api
490         
491         # Attributes
492         self.module = None
493         self.args = None
494
495         # These get initialised when the filter is connected
496         self.peer_guid = None
497         self.peer_proto = None
498         self.iface_guid = None
499         self.peer = None
500         self.iface = None
501     
502     def _get(what, self):
503         wref = self.iface
504         if wref:
505             wref = wref()
506         if wref:
507             return getattr(wref, what)
508         else:
509             return None
510
511     def _set(what, self, val):
512         wref = self.iface
513         if wref:
514             wref = wref()
515         if wref:
516             setattr(wref, what, val)
517     
518     tun_proto = property(
519         functools.partial(_get, 'tun_proto'),
520         functools.partial(_set, 'tun_proto') )
521     tun_addr = property(
522         functools.partial(_get, 'tun_addr'),
523         functools.partial(_set, 'tun_addr') )
524     tun_port = property(
525         functools.partial(_get, 'tun_port'),
526         functools.partial(_set, 'tun_port') )
527     tun_key = property(
528         functools.partial(_get, 'tun_key'),
529         functools.partial(_set, 'tun_key') )
530     tun_cipher = property(
531         functools.partial(_get, 'tun_cipher'),
532         functools.partial(_set, 'tun_cipher') )
533     
534     del _get
535     del _set
536
537     def remote_trace_path(self, whichtrace):
538         iface = self.iface()
539         if iface is not None:
540             return iface.remote_trace_path(whichtrace, self._TRACEMAP)
541         return None
542
543     def remote_trace_name(self, whichtrace):
544         iface = self.iface()
545         if iface is not None:
546             return iface.remote_trace_name(whichtrace, self._TRACEMAP)
547         return None
548
549     def sync_trace(self, local_dir, whichtrace):
550         iface = self.iface()
551         if iface is not None:
552             return iface.sync_trace(local_dir, whichtrace, self._TRACEMAP)
553         return None
554
555 class ClassQueueFilter(TunFilter):
556     _TRACEMAP = {
557         # tracename : (remotename, localname)
558         'dropped_stats' : ('dropped_stats', 'dropped_stats')
559     }
560     
561     def __init__(self, api=None):
562         super(ClassQueueFilter, self).__init__(api)
563         # Attributes
564         self.module = "classqueue.py"
565
566 class ToSQueueFilter(TunFilter):
567     def __init__(self, api=None):
568         super(ToSQueueFilter, self).__init__(api)
569         # Attributes
570         self.module = "tosqueue.py"
571