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