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