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