mininet / mininet / link.py @ c916f3ee
History | View | Annotate | Download (18.3 KB)
1 |
"""
|
---|---|
2 |
link.py: interface and link abstractions for mininet
|
3 |
|
4 |
It seems useful to bundle functionality for interfaces into a single
|
5 |
class.
|
6 |
|
7 |
Also it seems useful to enable the possibility of multiple flavors of
|
8 |
links, 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 |
|
15 |
Basic 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 |
|
21 |
Intf: basic interface object that can configure itself
|
22 |
TCIntf: interface with bandwidth limiting and delay via tc
|
23 |
|
24 |
Link: basic link class for creating veth pairs
|
25 |
"""
|
26 |
|
27 |
from mininet.log import info, error, debug |
28 |
from mininet.util import makeIntfPair, quietRun |
29 |
import mininet.node |
30 |
import re |
31 |
|
32 |
class Intf( object ): |
33 |
|
34 |
"Basic interface object that can configure itself."
|
35 |
|
36 |
def __init__( self, name, node=None, port=None, link=None, |
37 |
mac=None, **params ):
|
38 |
"""name: interface name (e.g. h1-eth0)
|
39 |
node: owning node (where this intf most likely lives)
|
40 |
link: parent link if we're part of a link
|
41 |
other arguments are passed to config()"""
|
42 |
self.node = node
|
43 |
self.name = name
|
44 |
self.link = link
|
45 |
self.mac = mac
|
46 |
self.ip, self.prefixLen = None, None |
47 |
|
48 |
# if interface is lo, we know the ip is 127.0.0.1.
|
49 |
# This saves an ifconfig command per node
|
50 |
if self.name == 'lo': |
51 |
self.ip = '127.0.0.1' |
52 |
# Add to node (and move ourselves if necessary )
|
53 |
node.addIntf( self, port=port )
|
54 |
# Save params for future reference
|
55 |
self.params = params
|
56 |
self.config( **params )
|
57 |
|
58 |
def cmd( self, *args, **kwargs ): |
59 |
"Run a command in our owning node"
|
60 |
return self.node.cmd( *args, **kwargs ) |
61 |
|
62 |
def ifconfig( self, *args ): |
63 |
"Configure ourselves using ifconfig"
|
64 |
return self.cmd( 'ifconfig', self.name, *args ) |
65 |
|
66 |
def setIP( self, ipstr, prefixLen=None ): |
67 |
"""Set our IP address"""
|
68 |
# This is a sign that we should perhaps rethink our prefix
|
69 |
# mechanism and/or the way we specify IP addresses
|
70 |
if '/' in ipstr: |
71 |
self.ip, self.prefixLen = ipstr.split( '/' ) |
72 |
return self.ifconfig( ipstr, 'up' ) |
73 |
else:
|
74 |
if prefixLen is None: |
75 |
raise Exception( 'No prefix length set for IP address %s' |
76 |
% ( ipstr, ) ) |
77 |
self.ip, self.prefixLen = ipstr, prefixLen |
78 |
return self.ifconfig( '%s/%s' % ( ipstr, prefixLen ) ) |
79 |
|
80 |
def setMAC( self, macstr ): |
81 |
"""Set the MAC address for an interface.
|
82 |
macstr: MAC address as string"""
|
83 |
self.mac = macstr
|
84 |
return ( self.ifconfig( 'down' ) + |
85 |
self.ifconfig( 'hw', 'ether', macstr ) + |
86 |
self.ifconfig( 'up' ) ) |
87 |
|
88 |
_ipMatchRegex = re.compile( r'\d+\.\d+\.\d+\.\d+' )
|
89 |
_macMatchRegex = re.compile( r'..:..:..:..:..:..' )
|
90 |
|
91 |
def updateIP( self ): |
92 |
"Return updated IP address based on ifconfig"
|
93 |
# use pexec instead of node.cmd so that we dont read
|
94 |
# backgrounded output from the cli.
|
95 |
ifconfig, _err, _exitCode = self.node.pexec(
|
96 |
'ifconfig %s' % self.name ) |
97 |
ips = self._ipMatchRegex.findall( ifconfig )
|
98 |
self.ip = ips[ 0 ] if ips else None |
99 |
return self.ip |
100 |
|
101 |
def updateMAC( self ): |
102 |
"Return updated MAC address based on ifconfig"
|
103 |
ifconfig = self.ifconfig()
|
104 |
macs = self._macMatchRegex.findall( ifconfig )
|
105 |
self.mac = macs[ 0 ] if macs else None |
106 |
return self.mac |
107 |
|
108 |
# Instead of updating ip and mac separately,
|
109 |
# use one ifconfig call to do it simultaneously.
|
110 |
# This saves an ifconfig command, which improves performance.
|
111 |
|
112 |
def updateAddr( self ): |
113 |
"Return IP address and MAC address based on ifconfig."
|
114 |
ifconfig = self.ifconfig()
|
115 |
ips = self._ipMatchRegex.findall( ifconfig )
|
116 |
macs = self._macMatchRegex.findall( ifconfig )
|
117 |
self.ip = ips[ 0 ] if ips else None |
118 |
self.mac = macs[ 0 ] if macs else None |
119 |
return self.ip, self.mac |
120 |
|
121 |
def IP( self ): |
122 |
"Return IP address"
|
123 |
return self.ip |
124 |
|
125 |
def MAC( self ): |
126 |
"Return MAC address"
|
127 |
return self.mac |
128 |
|
129 |
def isUp( self, setUp=False ): |
130 |
"Return whether interface is up"
|
131 |
if setUp:
|
132 |
cmdOutput = self.ifconfig( 'up' ) |
133 |
# no output indicates success
|
134 |
if cmdOutput:
|
135 |
error( "Error setting %s up: %s " % ( self.name, cmdOutput ) ) |
136 |
return False |
137 |
else:
|
138 |
return True |
139 |
else:
|
140 |
return "UP" in self.ifconfig() |
141 |
|
142 |
def rename( self, newname ): |
143 |
"Rename interface"
|
144 |
self.ifconfig( 'down' ) |
145 |
result = self.cmd( 'ip link set', self.name, 'name', newname ) |
146 |
self.name = newname
|
147 |
self.ifconfig( 'up' ) |
148 |
return result
|
149 |
|
150 |
# The reason why we configure things in this way is so
|
151 |
# That the parameters can be listed and documented in
|
152 |
# the config method.
|
153 |
# Dealing with subclasses and superclasses is slightly
|
154 |
# annoying, but at least the information is there!
|
155 |
|
156 |
def setParam( self, results, method, **param ): |
157 |
"""Internal method: configure a *single* parameter
|
158 |
results: dict of results to update
|
159 |
method: config method name
|
160 |
param: arg=value (ignore if value=None)
|
161 |
value may also be list or dict"""
|
162 |
name, value = param.items()[ 0 ]
|
163 |
f = getattr( self, method, None ) |
164 |
if not f or value is None: |
165 |
return
|
166 |
if isinstance( value, list ): |
167 |
result = f( *value ) |
168 |
elif isinstance( value, dict ): |
169 |
result = f( **value ) |
170 |
else:
|
171 |
result = f( value ) |
172 |
results[ name ] = result |
173 |
return result
|
174 |
|
175 |
def config( self, mac=None, ip=None, ifconfig=None, |
176 |
up=True, **_params ):
|
177 |
"""Configure Node according to (optional) parameters:
|
178 |
mac: MAC address
|
179 |
ip: IP address
|
180 |
ifconfig: arbitrary interface configuration
|
181 |
Subclasses should override this method and call
|
182 |
the parent class's config(**params)"""
|
183 |
# If we were overriding this method, we would call
|
184 |
# the superclass config method here as follows:
|
185 |
# r = Parent.config( **params )
|
186 |
r = {} |
187 |
self.setParam( r, 'setMAC', mac=mac ) |
188 |
self.setParam( r, 'setIP', ip=ip ) |
189 |
self.setParam( r, 'isUp', up=up ) |
190 |
self.setParam( r, 'ifconfig', ifconfig=ifconfig ) |
191 |
return r
|
192 |
|
193 |
def delete( self ): |
194 |
"Delete interface"
|
195 |
self.cmd( 'ip link del ' + self.name ) |
196 |
if self.node.inNamespace: |
197 |
# Link may have been dumped into root NS
|
198 |
quietRun( 'ip link del ' + self.name ) |
199 |
|
200 |
def status( self ): |
201 |
"Return intf status as a string"
|
202 |
links, _err, _result = self.node.pexec( 'ip link show' ) |
203 |
if self.name in links: |
204 |
return "OK" |
205 |
else:
|
206 |
return "MISSING" |
207 |
|
208 |
def __repr__( self ): |
209 |
return '<%s %s>' % ( self.__class__.__name__, self.name ) |
210 |
|
211 |
def __str__( self ): |
212 |
return self.name |
213 |
|
214 |
|
215 |
class TCIntf( Intf ): |
216 |
"""Interface customized by tc (traffic control) utility
|
217 |
Allows specification of bandwidth limits (various methods)
|
218 |
as well as delay, loss and max queue length"""
|
219 |
|
220 |
def bwCmds( self, bw=None, speedup=0, use_hfsc=False, use_tbf=False, |
221 |
latency_ms=None, enable_ecn=False, enable_red=False ): |
222 |
"Return tc commands to set bandwidth"
|
223 |
|
224 |
cmds, parent = [], ' root '
|
225 |
|
226 |
if bw and ( bw < 0 or bw > 1000 ): |
227 |
error( 'Bandwidth', bw, 'is outside range 0..1000 Mbps\n' ) |
228 |
|
229 |
elif bw is not None: |
230 |
# BL: this seems a bit brittle...
|
231 |
if ( speedup > 0 and |
232 |
self.node.name[0:1] == 's' ): |
233 |
bw = speedup |
234 |
# This may not be correct - we should look more closely
|
235 |
# at the semantics of burst (and cburst) to make sure we
|
236 |
# are specifying the correct sizes. For now I have used
|
237 |
# the same settings we had in the mininet-hifi code.
|
238 |
if use_hfsc:
|
239 |
cmds += [ '%s qdisc add dev %s root handle 5:0 hfsc default 1',
|
240 |
'%s class add dev %s parent 5:0 classid 5:1 hfsc sc '
|
241 |
+ 'rate %fMbit ul rate %fMbit' % ( bw, bw ) ]
|
242 |
elif use_tbf:
|
243 |
if latency_ms is None: |
244 |
latency_ms = 15 * 8 / bw |
245 |
cmds += [ '%s qdisc add dev %s root handle 5: tbf ' +
|
246 |
'rate %fMbit burst 15000 latency %fms' %
|
247 |
( bw, latency_ms ) ] |
248 |
else:
|
249 |
cmds += [ '%s qdisc add dev %s root handle 5:0 htb default 1',
|
250 |
'%s class add dev %s parent 5:0 classid 5:1 htb ' +
|
251 |
'rate %fMbit burst 15k' % bw ]
|
252 |
parent = ' parent 5:1 '
|
253 |
|
254 |
# ECN or RED
|
255 |
if enable_ecn:
|
256 |
cmds += [ '%s qdisc add dev %s' + parent +
|
257 |
'handle 6: red limit 1000000 ' +
|
258 |
'min 30000 max 35000 avpkt 1500 ' +
|
259 |
'burst 20 ' +
|
260 |
'bandwidth %fmbit probability 1 ecn' % bw ]
|
261 |
parent = ' parent 6: '
|
262 |
elif enable_red:
|
263 |
cmds += [ '%s qdisc add dev %s' + parent +
|
264 |
'handle 6: red limit 1000000 ' +
|
265 |
'min 30000 max 35000 avpkt 1500 ' +
|
266 |
'burst 20 ' +
|
267 |
'bandwidth %fmbit probability 1' % bw ]
|
268 |
parent = ' parent 6: '
|
269 |
return cmds, parent
|
270 |
|
271 |
@staticmethod
|
272 |
def delayCmds( parent, delay=None, jitter=None, |
273 |
loss=None, max_queue_size=None ): |
274 |
"Internal method: return tc commands for delay and loss"
|
275 |
cmds = [] |
276 |
if delay and delay < 0: |
277 |
error( 'Negative delay', delay, '\n' ) |
278 |
elif jitter and jitter < 0: |
279 |
error( 'Negative jitter', jitter, '\n' ) |
280 |
elif loss and ( loss < 0 or loss > 100 ): |
281 |
error( 'Bad loss percentage', loss, '%%\n' ) |
282 |
else:
|
283 |
# Delay/jitter/loss/max queue size
|
284 |
netemargs = '%s%s%s%s' % (
|
285 |
'delay %s ' % delay if delay is not None else '', |
286 |
'%s ' % jitter if jitter is not None else '', |
287 |
'loss %d ' % loss if loss is not None else '', |
288 |
'limit %d' % max_queue_size if max_queue_size is not None |
289 |
else '' ) |
290 |
if netemargs:
|
291 |
cmds = [ '%s qdisc add dev %s ' + parent +
|
292 |
' handle 10: netem ' +
|
293 |
netemargs ] |
294 |
parent = ' parent 10:1 '
|
295 |
return cmds, parent
|
296 |
|
297 |
def tc( self, cmd, tc='tc' ): |
298 |
"Execute tc command for our interface"
|
299 |
c = cmd % (tc, self) # Add in tc command and our name |
300 |
debug(" *** executing command: %s\n" % c)
|
301 |
return self.cmd( c ) |
302 |
|
303 |
def config( self, bw=None, delay=None, jitter=None, loss=None, |
304 |
disable_gro=True, speedup=0, use_hfsc=False, use_tbf=False, |
305 |
latency_ms=None, enable_ecn=False, enable_red=False, |
306 |
max_queue_size=None, **params ):
|
307 |
"Configure the port and set its properties."
|
308 |
|
309 |
result = Intf.config( self, **params)
|
310 |
|
311 |
# Disable GRO
|
312 |
if disable_gro:
|
313 |
self.cmd( 'ethtool -K %s gro off' % self ) |
314 |
|
315 |
# Optimization: return if nothing else to configure
|
316 |
# Question: what happens if we want to reset things?
|
317 |
if ( bw is None and not delay and not loss |
318 |
and max_queue_size is None ): |
319 |
return
|
320 |
|
321 |
# Clear existing configuration
|
322 |
tcoutput = self.tc( '%s qdisc show dev %s' ) |
323 |
if "priomap" not in tcoutput: |
324 |
cmds = [ '%s qdisc del dev %s root' ]
|
325 |
else:
|
326 |
cmds = [] |
327 |
|
328 |
# Bandwidth limits via various methods
|
329 |
bwcmds, parent = self.bwCmds( bw=bw, speedup=speedup,
|
330 |
use_hfsc=use_hfsc, use_tbf=use_tbf, |
331 |
latency_ms=latency_ms, |
332 |
enable_ecn=enable_ecn, |
333 |
enable_red=enable_red ) |
334 |
cmds += bwcmds |
335 |
|
336 |
# Delay/jitter/loss/max_queue_size using netem
|
337 |
delaycmds, parent = self.delayCmds( delay=delay, jitter=jitter,
|
338 |
loss=loss, |
339 |
max_queue_size=max_queue_size, |
340 |
parent=parent ) |
341 |
cmds += delaycmds |
342 |
|
343 |
# Ugly but functional: display configuration info
|
344 |
stuff = ( ( [ '%.2fMbit' % bw ] if bw is not None else [] ) + |
345 |
( [ '%s delay' % delay ] if delay is not None else [] ) + |
346 |
( [ '%s jitter' % jitter ] if jitter is not None else [] ) + |
347 |
( ['%d%% loss' % loss ] if loss is not None else [] ) + |
348 |
( [ 'ECN' ] if enable_ecn else [ 'RED' ] |
349 |
if enable_red else [] ) ) |
350 |
info( '(' + ' '.join( stuff ) + ') ' ) |
351 |
|
352 |
# Execute all the commands in our node
|
353 |
debug("at map stage w/cmds: %s\n" % cmds)
|
354 |
tcoutputs = [ self.tc(cmd) for cmd in cmds ] |
355 |
for output in tcoutputs: |
356 |
if output != '': |
357 |
error( "*** Error: %s" % output )
|
358 |
debug( "cmds:", cmds, '\n' ) |
359 |
debug( "outputs:", tcoutputs, '\n' ) |
360 |
result[ 'tcoutputs'] = tcoutputs
|
361 |
result[ 'parent' ] = parent
|
362 |
|
363 |
return result
|
364 |
|
365 |
|
366 |
class Link( object ): |
367 |
|
368 |
"""A basic link is just a veth pair.
|
369 |
Other types of links could be tunnels, link emulators, etc.."""
|
370 |
|
371 |
def __init__( self, node1, node2, port1=None, port2=None, |
372 |
intfName1=None, intfName2=None, addr1=None, addr2=None, |
373 |
intf=Intf, cls1=None, cls2=None, params1=None, |
374 |
params2=None ):
|
375 |
"""Create veth link to another node, making two new interfaces.
|
376 |
node1: first node
|
377 |
node2: second node
|
378 |
port1: node1 port number (optional)
|
379 |
port2: node2 port number (optional)
|
380 |
intf: default interface class/constructor
|
381 |
cls1, cls2: optional interface-specific constructors
|
382 |
intfName1: node1 interface name (optional)
|
383 |
intfName2: node2 interface name (optional)
|
384 |
params1: parameters for interface 1
|
385 |
params2: parameters for interface 2"""
|
386 |
# This is a bit awkward; it seems that having everything in
|
387 |
# params is more orthogonal, but being able to specify
|
388 |
# in-line arguments is more convenient! So we support both.
|
389 |
if params1 is None: |
390 |
params1 = {} |
391 |
if params2 is None: |
392 |
params2 = {} |
393 |
# Allow passing in params1=params2
|
394 |
if params2 is params1: |
395 |
params2 = dict( params1 )
|
396 |
if port1 is not None: |
397 |
params1[ 'port' ] = port1
|
398 |
if port2 is not None: |
399 |
params2[ 'port' ] = port2
|
400 |
if 'port' not in params1: |
401 |
params1[ 'port' ] = node1.newPort()
|
402 |
if 'port' not in params2: |
403 |
params2[ 'port' ] = node2.newPort()
|
404 |
if not intfName1: |
405 |
intfName1 = self.intfName( node1, params1[ 'port' ] ) |
406 |
if not intfName2: |
407 |
intfName2 = self.intfName( node2, params2[ 'port' ] ) |
408 |
|
409 |
self.makeIntfPair( intfName1, intfName2, addr1, addr2 )
|
410 |
|
411 |
if not cls1: |
412 |
cls1 = intf |
413 |
if not cls2: |
414 |
cls2 = intf |
415 |
|
416 |
intf1 = cls1( name=intfName1, node=node1, |
417 |
link=self, mac=addr1, **params1 )
|
418 |
intf2 = cls2( name=intfName2, node=node2, |
419 |
link=self, mac=addr2, **params2 )
|
420 |
|
421 |
# All we are is dust in the wind, and our two interfaces
|
422 |
self.intf1, self.intf2 = intf1, intf2 |
423 |
|
424 |
def intfName( self, node, n ): |
425 |
"Construct a canonical interface name node-ethN for interface n."
|
426 |
# Leave this as an instance method for now
|
427 |
assert self |
428 |
return node.name + '-eth' + repr( n ) |
429 |
|
430 |
@classmethod
|
431 |
def makeIntfPair( cls, intfname1, intfname2, addr1=None, addr2=None ): |
432 |
"""Create pair of interfaces
|
433 |
intfname1: name of interface 1
|
434 |
intfname2: name of interface 2
|
435 |
(override this method [and possibly delete()]
|
436 |
to change link type)"""
|
437 |
# Leave this as a class method for now
|
438 |
assert cls
|
439 |
return makeIntfPair( intfname1, intfname2, addr1, addr2 )
|
440 |
|
441 |
def delete( self ): |
442 |
"Delete this link"
|
443 |
self.intf1.delete()
|
444 |
self.intf2.delete()
|
445 |
|
446 |
def stop( self ): |
447 |
"Override to stop and clean up link as needed"
|
448 |
pass
|
449 |
|
450 |
def status( self ): |
451 |
"Return link status as a string"
|
452 |
return "(%s %s)" % ( self.intf1.status(), self.intf2.status() ) |
453 |
|
454 |
def __str__( self ): |
455 |
return '%s<->%s' % ( self.intf1, self.intf2 ) |
456 |
|
457 |
|
458 |
class OVSIntf( Intf ): |
459 |
"Patch interface on an OVSSwitch"
|
460 |
|
461 |
def ifconfig( self, cmd ): |
462 |
if cmd == 'up': |
463 |
"OVSIntf is always up"
|
464 |
return
|
465 |
else:
|
466 |
raise Exception( 'OVSIntf cannot do ifconfig ' + cmd ) |
467 |
|
468 |
|
469 |
class OVSLink( Link ): |
470 |
"Link that makes patch links between OVSSwitches"
|
471 |
|
472 |
def __init__( self, node1, node2, **kwargs ): |
473 |
"See Link.__init__() for options"
|
474 |
self.isPatchLink = False |
475 |
if ( type( node1 ) is mininet.node.OVSSwitch and |
476 |
type( node2 ) is mininet.node.OVSSwitch ): |
477 |
self.isPatchLink = True |
478 |
kwargs.update( cls1=OVSIntf, cls2=OVSIntf ) |
479 |
Link.__init__( self, node1, node2, **kwargs )
|
480 |
|
481 |
def makeIntfPair( self, *args, **kwargs ): |
482 |
"Usually delegated to OVSSwitch"
|
483 |
if self.isPatchLink: |
484 |
return None, None |
485 |
else:
|
486 |
return Link.makeIntfPair( *args, **kwargs )
|
487 |
|
488 |
|
489 |
class TCLink( Link ): |
490 |
"Link with symmetric TC interfaces configured via opts"
|
491 |
def __init__( self, node1, node2, port1=None, port2=None, |
492 |
intfName1=None, intfName2=None, |
493 |
addr1=None, addr2=None, **params ): |
494 |
Link.__init__( self, node1, node2, port1=port1, port2=port2,
|
495 |
intfName1=intfName1, intfName2=intfName2, |
496 |
cls1=TCIntf, |
497 |
cls2=TCIntf, |
498 |
addr1=addr1, addr2=addr2, |
499 |
params1=params, |
500 |
params2=params ) |