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