30895b86585e85fe5dac828f5ba06c8b5b99e10e
[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 _impl_instance(self, home_path, listening):
172         impl = self._PROTO_MAP[self.peer_proto](
173             self, self.peer_iface, home_path, self.tun_key, listening)
174         impl.port = self.tun_port
175         return impl
176     
177     def prepare(self, home_path, listening):
178         if not self.peer_iface and (self.peer_proto and (listening or (self.peer_addr and self.peer_port))):
179             # Ad-hoc peer_iface
180             self.peer_iface = CrossIface(
181                 self.peer_proto,
182                 self.peer_addr,
183                 self.peer_port)
184         if self.peer_iface:
185             if not self.peer_proto_impl:
186                 self.peer_proto_impl = self._impl_instance(home_path, listening)
187             self.peer_proto_impl.prepare()
188     
189     def setup(self):
190         if self.peer_proto_impl:
191             self.peer_proto_impl.setup()
192     
193     def cleanup(self):
194         if self.peer_proto_impl:
195             self.peer_proto_impl.shutdown()
196             self.peer_proto_impl = None
197
198     def sync_trace(self, local_dir, whichtrace):
199         if self.peer_proto_impl:
200             return self.peer_proto_impl.sync_trace(local_dir, whichtrace)
201         else:
202             return None
203
204 class TapIface(TunIface):
205     _PROTO_MAP = tunproto.TAP_PROTO_MAP
206     _KIND = 'TAP'
207
208 # Yep, it does nothing - yet
209 class Internet(object):
210     def __init__(self, api=None):
211         if not api:
212             api = plcapi.PLCAPI()
213         self._api = api
214
215 class NetPipe(object):
216     def __init__(self, api=None):
217         if not api:
218             api = plcapi.PLCAPI()
219         self._api = api
220
221         # Attributes
222         self.mode = None
223         self.addrList = None
224         self.portList = None
225         
226         self.plrIn = None
227         self.bwIn = None
228         self.delayIn = None
229
230         self.plrOut = None
231         self.bwOut = None
232         self.delayOut = None
233         
234         # These get initialized when the pipe is connected to its node
235         self.node = None
236         self.configured = False
237     
238     def validate(self):
239         if not self.mode:
240             raise RuntimeError, "Undefined NetPipe mode"
241         if not self.portList:
242             raise RuntimeError, "Undefined NetPipe port list - must always define the scope"
243         if not (self.plrIn or self.bwIn or self.delayIn):
244             raise RuntimeError, "Undefined NetPipe inbound characteristics"
245         if not (self.plrOut or self.bwOut or self.delayOut):
246             raise RuntimeError, "Undefined NetPipe outbound characteristics"
247         if not self.node:
248             raise RuntimeError, "Unconnected NetPipe"
249     
250     def _add_pipedef(self, bw, plr, delay, options):
251         if delay:
252             options.extend(("delay","%dms" % (delay,)))
253         if bw:
254             options.extend(("bw","%.8fMbit/s" % (bw,)))
255         if plr:
256             options.extend(("plr","%.8f" % (plr,)))
257     
258     def _get_ruledef(self):
259         scope = "%s%s%s" % (
260             self.portList,
261             "@" if self.addrList else "",
262             self.addrList or "",
263         )
264         
265         options = []
266         if self.bwIn or self.plrIn or self.delayIn:
267             options.append("IN")
268             self._add_pipedef(self.bwIn, self.plrIn, self.delayIn, options)
269         if self.bwOut or self.plrOut or self.delayOut:
270             options.append("OUT")
271             self._add_pipedef(self.bwOut, self.plrOut, self.delayOut, options)
272         options = ' '.join(options)
273         
274         return (scope,options)
275
276     def configure(self):
277         # set up rule
278         scope, options = self._get_ruledef()
279         command = "sudo -S netconfig config %s %s %s" % (self.mode, scope, options)
280         
281         (out,err),proc = server.popen_ssh_command(
282             command,
283             host = self.node.hostname,
284             port = None,
285             user = self.node.slicename,
286             agent = None,
287             ident_key = self.node.ident_path,
288             server_key = self.node.server_key
289             )
290     
291         if proc.wait():
292             raise RuntimeError, "Failed instal build sources: %s %s" % (out,err,)
293         
294         # we have to clean up afterwards
295         self.configured = True
296     
297     def refresh(self):
298         if self.configured:
299             # refresh rule
300             scope, options = self._get_ruledef()
301             command = "sudo -S netconfig refresh %s %s %s" % (self.mode, scope, options)
302             
303             (out,err),proc = server.popen_ssh_command(
304                 command,
305                 host = self.node.hostname,
306                 port = None,
307                 user = self.node.slicename,
308                 agent = None,
309                 ident_key = self.node.ident_path,
310                 server_key = self.node.server_key
311                 )
312         
313             if proc.wait():
314                 raise RuntimeError, "Failed instal build sources: %s %s" % (out,err,)
315     
316     def cleanup(self):
317         if self.configured:
318             # remove rule
319             scope, options = self._get_ruledef()
320             command = "sudo -S netconfig delete %s %s" % (self.mode, scope)
321             
322             (out,err),proc = server.popen_ssh_command(
323                 command,
324                 host = self.node.hostname,
325                 port = None,
326                 user = self.node.slicename,
327                 agent = None,
328                 ident_key = self.node.ident_path,
329                 server_key = self.node.server_key
330                 )
331         
332             if proc.wait():
333                 raise RuntimeError, "Failed instal build sources: %s %s" % (out,err,)
334             
335             self.configured = False
336     
337     def sync_trace(self, local_dir, whichtrace):
338         if whichtrace != 'netpipeStats':
339             raise ValueError, "Unsupported trace %s" % (whichtrace,)
340         
341         local_path = os.path.join(local_dir, "netpipe_stats_%s" % (self.mode,))
342         
343         # create parent local folders
344         proc = subprocess.Popen(
345             ["mkdir", "-p", os.path.dirname(local_path)],
346             stdout = open("/dev/null","w"),
347             stdin = open("/dev/null","r"))
348
349         if proc.wait():
350             raise RuntimeError, "Failed to synchronize trace: %s %s" % (out,err,)
351         
352         (out,err),proc = server.popen_ssh_command(
353             "echo 'Rules:' ; sudo -S netconfig show rules ; echo 'Pipes:' ; sudo -S netconfig show pipes",
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 to synchronize trace: %s %s" % (out,err,)
364         
365         # dump results to file
366         f = open(local_path, "wb")
367         f.write(err or "")
368         f.write(out or "")
369         f.close()
370         
371         return local_path
372