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