blob: 6c9dd7dcdfa0d48c2aed1b37de8c5be0aa01a188 [file] [log] [blame]
carlosmscabralf40ecd12013-02-01 18:15:58 -02001"""
2link.py: interface and link abstractions for mininet
3
4It seems useful to bundle functionality for interfaces into a single
5class.
6
7Also it seems useful to enable the possibility of multiple flavors of
8links, including:
9
10- simple veth pairs
11- tunneled links
12- patchable links (which can be disconnected and reconnected via a patchbay)
13- link simulators (e.g. wireless)
14
15Basic division of labor:
16
17 Nodes: know how to execute commands
18 Intfs: know how to configure themselves
19 Links: know how to connect nodes together
20
21Intf: basic interface object that can configure itself
22TCIntf: interface with bandwidth limiting and delay via tc
23
24Link: basic link class for creating veth pairs
25"""
26
27from mininet.log import info, error, debug
28from mininet.util import makeIntfPair
29from time import sleep
30import re
31
32class Intf( object ):
33
34 "Basic interface object that can configure itself."
35
36 def __init__( self, name, node=None, port=None, link=None, **params ):
37 """name: interface name (e.g. h1-eth0)
38 node: owning node (where this intf most likely lives)
39 link: parent link if we're part of a link
40 other arguments are passed to config()"""
41 self.node = node
42 self.name = name
43 self.link = link
44 self.mac, self.ip, self.prefixLen = None, None, None
45 # Add to node (and move ourselves if necessary )
46 node.addIntf( self, port=port )
47 # Save params for future reference
48 self.params = params
49 self.config( **params )
50
51 def cmd( self, *args, **kwargs ):
52 "Run a command in our owning node"
53 return self.node.cmd( *args, **kwargs )
54
55 def ifconfig( self, *args ):
56 "Configure ourselves using ifconfig"
57 return self.cmd( 'ifconfig', self.name, *args )
58
59 def setIP( self, ipstr, prefixLen=None ):
60 """Set our IP address"""
61 # This is a sign that we should perhaps rethink our prefix
62 # mechanism and/or the way we specify IP addresses
63 if '/' in ipstr:
64 self.ip, self.prefixLen = ipstr.split( '/' )
65 return self.ifconfig( ipstr, 'up' )
66 else:
67 self.ip, self.prefixLen = ipstr, prefixLen
68 return self.ifconfig( '%s/%s' % ( ipstr, prefixLen ) )
69
70 def setMAC( self, macstr ):
71 """Set the MAC address for an interface.
72 macstr: MAC address as string"""
73 self.mac = macstr
74 return ( self.ifconfig( 'down' ) +
75 self.ifconfig( 'hw', 'ether', macstr ) +
76 self.ifconfig( 'up' ) )
77
78 _ipMatchRegex = re.compile( r'\d+\.\d+\.\d+\.\d+' )
79 _macMatchRegex = re.compile( r'..:..:..:..:..:..' )
80
81 def updateIP( self ):
82 "Return updated IP address based on ifconfig"
83 ifconfig = self.ifconfig()
84 ips = self._ipMatchRegex.findall( ifconfig )
85 self.ip = ips[ 0 ] if ips else None
86 return self.ip
87
88 def updateMAC( self ):
89 "Return updated MAC address based on ifconfig"
90 ifconfig = self.ifconfig()
91 macs = self._macMatchRegex.findall( ifconfig )
92 self.mac = macs[ 0 ] if macs else None
93 return self.mac
94
95 def IP( self ):
96 "Return IP address"
97 return self.ip
98
99 def MAC( self ):
100 "Return MAC address"
101 return self.mac
102
103 def isUp( self, setUp=False ):
104 "Return whether interface is up"
105 if setUp:
106 self.ifconfig( 'up' )
107 return "UP" in self.ifconfig()
108
109 def rename( self, newname ):
110 "Rename interface"
111 self.ifconfig( 'down' )
112 result = self.cmd( 'ip link set', self.name, 'name', newname )
113 self.name = newname
114 self.ifconfig( 'up' )
115 return result
116
117 # The reason why we configure things in this way is so
118 # That the parameters can be listed and documented in
119 # the config method.
120 # Dealing with subclasses and superclasses is slightly
121 # annoying, but at least the information is there!
122
123 def setParam( self, results, method, **param ):
124 """Internal method: configure a *single* parameter
125 results: dict of results to update
126 method: config method name
127 param: arg=value (ignore if value=None)
128 value may also be list or dict"""
129 name, value = param.items()[ 0 ]
130 f = getattr( self, method, None )
131 if not f or value is None:
132 return
133 if type( value ) is list:
134 result = f( *value )
135 elif type( value ) is dict:
136 result = f( **value )
137 else:
138 result = f( value )
139 results[ name ] = result
140 return result
141
142 def config( self, mac=None, ip=None, ifconfig=None,
143 up=True, **_params ):
144 """Configure Node according to (optional) parameters:
145 mac: MAC address
146 ip: IP address
147 ifconfig: arbitrary interface configuration
148 Subclasses should override this method and call
149 the parent class's config(**params)"""
150 # If we were overriding this method, we would call
151 # the superclass config method here as follows:
152 # r = Parent.config( **params )
153 r = {}
154 self.setParam( r, 'setMAC', mac=mac )
155 self.setParam( r, 'setIP', ip=ip )
156 self.setParam( r, 'isUp', up=up )
157 self.setParam( r, 'ifconfig', ifconfig=ifconfig )
158 self.updateIP()
159 self.updateMAC()
160 return r
161
162 def delete( self ):
163 "Delete interface"
164 self.cmd( 'ip link del ' + self.name )
165 # Does it help to sleep to let things run?
166 sleep( 0.001 )
167
168 def __repr__( self ):
169 return '<%s %s>' % ( self.__class__.__name__, self.name )
170
171 def __str__( self ):
172 return self.name
173
174
175class TCIntf( Intf ):
176 """Interface customized by tc (traffic control) utility
177 Allows specification of bandwidth limits (various methods)
178 as well as delay, loss and max queue length"""
179
180 def bwCmds( self, bw=None, speedup=0, use_hfsc=False, use_tbf=False,
181 latency_ms=None, enable_ecn=False, enable_red=False ):
182 "Return tc commands to set bandwidth"
183
carlosmscabrale121a7b2013-02-18 18:14:53 -0300184
carlosmscabralf40ecd12013-02-01 18:15:58 -0200185 cmds, parent = [], ' root '
186
187 if bw and ( bw < 0 or bw > 1000 ):
188 error( 'Bandwidth', bw, 'is outside range 0..1000 Mbps\n' )
189
190 elif bw is not None:
191 # BL: this seems a bit brittle...
192 if ( speedup > 0 and
193 self.node.name[0:1] == 's' ):
194 bw = speedup
195 # This may not be correct - we should look more closely
196 # at the semantics of burst (and cburst) to make sure we
197 # are specifying the correct sizes. For now I have used
198 # the same settings we had in the mininet-hifi code.
199 if use_hfsc:
200 cmds += [ '%s qdisc add dev %s root handle 1:0 hfsc default 1',
201 '%s class add dev %s parent 1:0 classid 1:1 hfsc sc '
202 + 'rate %fMbit ul rate %fMbit' % ( bw, bw ) ]
203 elif use_tbf:
204 if latency_ms is None:
205 latency_ms = 15 * 8 / bw
206 cmds += [ '%s qdisc add dev %s root handle 1: tbf ' +
207 'rate %fMbit burst 15000 latency %fms' %
208 ( bw, latency_ms ) ]
209 else:
210 cmds += [ '%s qdisc add dev %s root handle 1:0 htb default 1',
211 '%s class add dev %s parent 1:0 classid 1:1 htb ' +
212 'rate %fMbit burst 15k' % bw ]
213 parent = ' parent 1:1 '
214
215 # ECN or RED
216 if enable_ecn:
217 cmds += [ '%s qdisc add dev %s' + parent +
218 'handle 10: red limit 1000000 ' +
219 'min 30000 max 35000 avpkt 1500 ' +
220 'burst 20 ' +
221 'bandwidth %fmbit probability 1 ecn' % bw ]
222 parent = ' parent 10: '
223 elif enable_red:
224 cmds += [ '%s qdisc add dev %s' + parent +
225 'handle 10: red limit 1000000 ' +
226 'min 30000 max 35000 avpkt 1500 ' +
227 'burst 20 ' +
228 'bandwidth %fmbit probability 1' % bw ]
229 parent = ' parent 10: '
230 return cmds, parent
231
232 @staticmethod
233 def delayCmds( parent, delay=None, jitter=None,
234 loss=None, max_queue_size=None ):
235 "Internal method: return tc commands for delay and loss"
carlosmscabrale121a7b2013-02-18 18:14:53 -0300236
carlosmscabralf40ecd12013-02-01 18:15:58 -0200237 cmds = []
238 if delay and delay < 0:
239 error( 'Negative delay', delay, '\n' )
240 elif jitter and jitter < 0:
241 error( 'Negative jitter', jitter, '\n' )
242 elif loss and ( loss < 0 or loss > 100 ):
243 error( 'Bad loss percentage', loss, '%%\n' )
244 else:
245 # Delay/jitter/loss/max queue size
246 netemargs = '%s%s%s%s' % (
247 'delay %s ' % delay if delay is not None else '',
248 '%s ' % jitter if jitter is not None else '',
249 'loss %d ' % loss if loss is not None else '',
250 'limit %d' % max_queue_size if max_queue_size is not None
251 else '' )
252 if netemargs:
253 cmds = [ '%s qdisc add dev %s ' + parent +
254 ' handle 10: netem ' +
255 netemargs ]
256 return cmds
257
258 def tc( self, cmd, tc='tc' ):
259 "Execute tc command for our interface"
260 c = cmd % (tc, self) # Add in tc command and our name
261 debug(" *** executing command: %s\n" % c)
262 return self.cmd( c )
263
264 def config( self, bw=None, delay=None, jitter=None, loss=None,
265 disable_gro=True, speedup=0, use_hfsc=False, use_tbf=False,
266 latency_ms=None, enable_ecn=False, enable_red=False,
267 max_queue_size=None, **params ):
268 "Configure the port and set its properties."
269
270 result = Intf.config( self, **params)
271
272 # Disable GRO
273 if disable_gro:
274 self.cmd( 'ethtool -K %s gro off' % self )
275
276 # Optimization: return if nothing else to configure
277 # Question: what happens if we want to reset things?
278 if ( bw is None and not delay and not loss
279 and max_queue_size is None ):
280 return
281
282 # Clear existing configuration
283 cmds = [ '%s qdisc del dev %s root' ]
284
285 # Bandwidth limits via various methods
286 bwcmds, parent = self.bwCmds( bw=bw, speedup=speedup,
287 use_hfsc=use_hfsc, use_tbf=use_tbf,
288 latency_ms=latency_ms,
289 enable_ecn=enable_ecn,
290 enable_red=enable_red )
291 cmds += bwcmds
292
293 # Delay/jitter/loss/max_queue_size using netem
294 cmds += self.delayCmds( delay=delay, jitter=jitter, loss=loss,
295 max_queue_size=max_queue_size,
296 parent=parent )
297
298 # Ugly but functional: display configuration info
299 stuff = ( ( [ '%.2fMbit' % bw ] if bw is not None else [] ) +
300 ( [ '%s delay' % delay ] if delay is not None else [] ) +
301 ( [ '%s jitter' % jitter ] if jitter is not None else [] ) +
302 ( ['%d%% loss' % loss ] if loss is not None else [] ) +
303 ( [ 'ECN' ] if enable_ecn else [ 'RED' ]
304 if enable_red else [] ) )
305 info( '(' + ' '.join( stuff ) + ') ' )
306
307 # Execute all the commands in our node
308 debug("at map stage w/cmds: %s\n" % cmds)
309 tcoutputs = [ self.tc(cmd) for cmd in cmds ]
310 debug( "cmds:", cmds, '\n' )
311 debug( "outputs:", tcoutputs, '\n' )
312 result[ 'tcoutputs'] = tcoutputs
313
314 return result
315
316
317class Link( object ):
318
319 """A basic link is just a veth pair.
320 Other types of links could be tunnels, link emulators, etc.."""
321
322 def __init__( self, node1, node2, port1=None, port2=None,
323 intfName1=None, intfName2=None,
324 intf=Intf, cls1=None, cls2=None, params1=None,
325 params2=None ):
326 """Create veth link to another node, making two new interfaces.
327 node1: first node
328 node2: second node
329 port1: node1 port number (optional)
330 port2: node2 port number (optional)
331 intf: default interface class/constructor
332 cls1, cls2: optional interface-specific constructors
333 intfName1: node1 interface name (optional)
334 intfName2: node2 interface name (optional)
335 params1: parameters for interface 1
336 params2: parameters for interface 2"""
337 # This is a bit awkward; it seems that having everything in
338 # params would be more orthogonal, but being able to specify
339 # in-line arguments is more convenient!
340 if port1 is None:
341 port1 = node1.newPort()
342 if port2 is None:
343 port2 = node2.newPort()
344 if not intfName1:
345 intfName1 = self.intfName( node1, port1 )
346 if not intfName2:
347 intfName2 = self.intfName( node2, port2 )
348
349 self.makeIntfPair( intfName1, intfName2 )
350
351 if not cls1:
352 cls1 = intf
353 if not cls2:
354 cls2 = intf
355 if not params1:
356 params1 = {}
357 if not params2:
358 params2 = {}
359
360 intf1 = cls1( name=intfName1, node=node1, port=port1,
361 link=self, **params1 )
362 intf2 = cls2( name=intfName2, node=node2, port=port2,
363 link=self, **params2 )
364
365 # All we are is dust in the wind, and our two interfaces
366 self.intf1, self.intf2 = intf1, intf2
367
368 @classmethod
369 def intfName( cls, node, n ):
370 "Construct a canonical interface name node-ethN for interface n."
371 return node.name + '-eth' + repr( n )
372
373 @classmethod
374 def makeIntfPair( cls, intf1, intf2 ):
375 """Create pair of interfaces
376 intf1: name of interface 1
377 intf2: name of interface 2
378 (override this class method [and possibly delete()]
379 to change link type)"""
380 makeIntfPair( intf1, intf2 )
381
382 def delete( self ):
383 "Delete this link"
384 self.intf1.delete()
385 self.intf2.delete()
386
387 def __str__( self ):
388 return '%s<->%s' % ( self.intf1, self.intf2 )
389
390class TCLink( Link ):
391 "Link with symmetric TC interfaces configured via opts"
392 def __init__( self, node1, node2, port1=None, port2=None,
393 intfName1=None, intfName2=None, **params ):
394 Link.__init__( self, node1, node2, port1=port1, port2=port2,
395 intfName1=intfName1, intfName2=intfName2,
396 cls1=TCIntf,
397 cls2=TCIntf,
398 params1=params,
399 params2=params)