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