carlosmscabral | f40ecd1 | 2013-02-01 18:15:58 -0200 | [diff] [blame] | 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 |
| 29 | from time import sleep |
| 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, **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 | |
| 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 | |
carlosmscabral | e121a7b | 2013-02-18 18:14:53 -0300 | [diff] [blame] | 184 | |
carlosmscabral | f40ecd1 | 2013-02-01 18:15:58 -0200 | [diff] [blame] | 185 | 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" |
carlosmscabral | e121a7b | 2013-02-18 18:14:53 -0300 | [diff] [blame] | 236 | |
carlosmscabral | f40ecd1 | 2013-02-01 18:15:58 -0200 | [diff] [blame] | 237 | 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 | |
| 317 | class 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 | |
| 390 | class 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) |