Ticket #14: WIP, only intra-PL TUN connections, required groundwork for cross-backend...
[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 TunIface(object):
87     def __init__(self, api=None):
88         if not api:
89             api = plcapi.PLCAPI()
90         self._api = api
91         
92         # Attributes
93         self.address = None
94         self.netprefix = None
95         self.netmask = None
96         
97         self.up = None
98         self.device_name = None
99         self.mtu = None
100         self.snat = False
101         self.txqueuelen = None
102         
103         # Enabled traces
104         self.capture = False
105
106         # These get initialized when the iface is connected to its node
107         self.node = None
108         
109         # These get initialized when the iface is configured
110         self.external_iface = None
111         
112         # These get initialized when the iface is configured
113         # They're part of the TUN standard attribute set
114         self.tun_port = None
115         self.tun_addr = None
116         
117         # These get initialized when the iface is connected to its peer
118         self.peer_iface = None
119         self.peer_proto = None
120         self.peer_proto_impl = None
121
122         # same as peer proto, but for execute-time standard attribute lookups
123         self.tun_proto = None 
124
125     def __str__(self):
126         return "%s<ip:%s/%s %s%s>" % (
127             self.__class__.__name__,
128             self.address, self.netprefix,
129             " up" if self.up else " down",
130             " snat" if self.snat else "",
131         )
132
133     def add_address(self, address, netprefix, broadcast):
134         if (self.address or self.netprefix or self.netmask) is not None:
135             raise RuntimeError, "Cannot add more than one address to TUN interfaces"
136         if broadcast:
137             raise ValueError, "TUN interfaces cannot broadcast in PlanetLab"
138         
139         self.address = address
140         self.netprefix = netprefix
141         self.netmask = ipaddr2.ipv4_mask2dot(netprefix)
142     
143     def validate(self):
144         if not self.node:
145             raise RuntimeError, "Unconnected TUN iface - missing node"
146         if self.peer_iface and self.peer_proto not in tunproto.PROTO_MAP:
147             raise RuntimeError, "Unsupported tunnelling protocol: %s" % (self.peer_proto,)
148         if not self.address or not self.netprefix or not self.netmask:
149             raise RuntimeError, "Misconfigured TUN iface - missing address"
150     
151     def prepare(self, home_path, listening):
152         if self.peer_iface:
153             if not self.peer_proto_impl:
154                 self.peer_proto_impl = tunproto.PROTO_MAP[self.peer_proto](
155                     self, self.peer_iface, home_path, listening)
156                 self.peer_proto_impl.port = self.tun_port
157             self.peer_proto_impl.prepare()
158     
159     def setup(self):
160         if self.peer_iface:
161             self.peer_proto_impl.setup()
162     
163     def destroy(self):
164         if self.peer_proto_impl:
165             self.peer_proto_impl.shutdown()
166             self.peer_proto_impl = None
167
168     def sync_trace(self, local_dir, whichtrace):
169         if self.peer_proto_impl:
170             return self.peer_proto_impl.sync_trace(local_dir, whichtrace)
171         else:
172             return None
173
174 # Yep, it does nothing - yet
175 class Internet(object):
176     def __init__(self, api=None):
177         if not api:
178             api = plcapi.PLCAPI()
179         self._api = api
180
181 class NetPipe(object):
182     def __init__(self, api=None):
183         if not api:
184             api = plcapi.PLCAPI()
185         self._api = api
186
187         # Attributes
188         self.mode = None
189         self.addrList = None
190         self.portList = None
191         
192         self.plrIn = None
193         self.bwIn = None
194         self.delayIn = None
195
196         self.plrOut = None
197         self.bwOut = None
198         self.delayOut = None
199         
200         # These get initialized when the pipe is connected to its node
201         self.node = None
202         self.configured = False
203     
204     def validate(self):
205         if not self.mode:
206             raise RuntimeError, "Undefined NetPipe mode"
207         if not self.portList:
208             raise RuntimeError, "Undefined NetPipe port list - must always define the scope"
209         if not (self.plrIn or self.bwIn or self.delayIn):
210             raise RuntimeError, "Undefined NetPipe inbound characteristics"
211         if not (self.plrOut or self.bwOut or self.delayOut):
212             raise RuntimeError, "Undefined NetPipe outbound characteristics"
213         if not self.node:
214             raise RuntimeError, "Unconnected NetPipe"
215     
216     def _add_pipedef(self, bw, plr, delay, options):
217         if delay:
218             options.extend(("delay","%dms" % (delay,)))
219         if bw:
220             options.extend(("bw","%.8fMbit/s" % (bw,)))
221         if plr:
222             options.extend(("plr","%.8f" % (plr,)))
223     
224     def _get_ruledef(self):
225         scope = "%s%s%s" % (
226             self.portList,
227             "@" if self.addrList else "",
228             self.addrList or "",
229         )
230         
231         options = []
232         if self.bwIn or self.plrIn or self.delayIn:
233             options.append("IN")
234             self._add_pipedef(self.bwIn, self.plrIn, self.delayIn, options)
235         if self.bwOut or self.plrOut or self.delayOut:
236             options.append("OUT")
237             self._add_pipedef(self.bwOut, self.plrOut, self.delayOut, options)
238         options = ' '.join(options)
239         
240         return (scope,options)
241
242     def configure(self):
243         # set up rule
244         scope, options = self._get_ruledef()
245         command = "sudo -S netconfig config %s %s %s" % (self.mode, scope, options)
246         
247         (out,err),proc = server.popen_ssh_command(
248             command,
249             host = self.node.hostname,
250             port = None,
251             user = self.node.slicename,
252             agent = None,
253             ident_key = self.node.ident_path,
254             server_key = self.node.server_key
255             )
256     
257         if proc.wait():
258             raise RuntimeError, "Failed instal build sources: %s %s" % (out,err,)
259         
260         # we have to clean up afterwards
261         self.configured = True
262     
263     def refresh(self):
264         if self.configured:
265             # refresh rule
266             scope, options = self._get_ruledef()
267             command = "sudo -S netconfig refresh %s %s %s" % (self.mode, scope, options)
268             
269             (out,err),proc = server.popen_ssh_command(
270                 command,
271                 host = self.node.hostname,
272                 port = None,
273                 user = self.node.slicename,
274                 agent = None,
275                 ident_key = self.node.ident_path,
276                 server_key = self.node.server_key
277                 )
278         
279             if proc.wait():
280                 raise RuntimeError, "Failed instal build sources: %s %s" % (out,err,)
281     
282     def cleanup(self):
283         if self.configured:
284             # remove rule
285             scope, options = self._get_ruledef()
286             command = "sudo -S netconfig delete %s %s" % (self.mode, scope)
287             
288             (out,err),proc = server.popen_ssh_command(
289                 command,
290                 host = self.node.hostname,
291                 port = None,
292                 user = self.node.slicename,
293                 agent = None,
294                 ident_key = self.node.ident_path,
295                 server_key = self.node.server_key
296                 )
297         
298             if proc.wait():
299                 raise RuntimeError, "Failed instal build sources: %s %s" % (out,err,)
300             
301             self.configured = False
302     
303     def sync_trace(self, local_dir, whichtrace):
304         if whichtrace != 'netpipeStats':
305             raise ValueError, "Unsupported trace %s" % (whichtrace,)
306         
307         local_path = os.path.join(local_dir, "netpipe_stats_%s" % (self.mode,))
308         
309         # create parent local folders
310         proc = subprocess.Popen(
311             ["mkdir", "-p", os.path.dirname(local_path)],
312             stdout = open("/dev/null","w"),
313             stdin = open("/dev/null","r"))
314
315         if proc.wait():
316             raise RuntimeError, "Failed to synchronize trace: %s %s" % (out,err,)
317         
318         (out,err),proc = server.popen_ssh_command(
319             "echo 'Rules:' ; sudo -S netconfig show rules ; echo 'Pipes:' ; sudo -S netconfig show pipes",
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 to synchronize trace: %s %s" % (out,err,)
330         
331         # dump results to file
332         f = open(local_path, "wb")
333         f.write(err or "")
334         f.write(out or "")
335         f.close()
336         
337         return local_path
338