Statistics
| Branch: | Tag: | Revision:

mininet / mininet / link.py @ 474f6860

History | View | Annotate | Download (17.1 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,
36
                  mac=None, srcNode=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 = mac
45
        self.ip, self.prefixLen = None, None
46
        
47
        # if interface is lo, we know the ip is 127.0.0.1.
48
        # This saves an ifconfig command per node
49
        if self.name == 'lo':
50
            self.ip = '127.0.0.1'
51
        # Add to node (and move ourselves if necessary )
52
        node.addIntf( self, port=port )
53
        # Save params for future reference
54
        self.params = params
55
        self.config( **params )
56

    
57
    def cmd( self, *args, **kwargs ):
58
        "Run a command in our owning node"
59
        return self.node.cmd( *args, **kwargs )
60

    
61
    def ifconfig( self, *args ):
62
        "Configure ourselves using ifconfig"
63
        return self.cmd( 'ifconfig', self.name, *args )
64

    
65
    def setIP( self, ipstr, prefixLen=None ):
66
        """Set our IP address"""
67
        # This is a sign that we should perhaps rethink our prefix
68
        # mechanism and/or the way we specify IP addresses
69
        if '/' in ipstr:
70
            self.ip, self.prefixLen = ipstr.split( '/' )
71
            return self.ifconfig( ipstr, 'up' )
72
        else:
73
            if prefixLen is None:
74
                raise Exception( 'No prefix length set for IP address %s' % ( ipstr, ) )
75
            self.ip, self.prefixLen = ipstr, prefixLen
76
            return self.ifconfig( '%s/%s' % ( ipstr, prefixLen ) )
77

    
78
    def setMAC( self, macstr ):
79
        """Set the MAC address for an interface.
80
           macstr: MAC address as string"""
81
        self.mac = macstr
82
        return ( self.ifconfig( 'down' ) +
83
                 self.ifconfig( 'hw', 'ether', macstr ) +
84
                 self.ifconfig( 'up' ) )
85

    
86
    _ipMatchRegex = re.compile( r'\d+\.\d+\.\d+\.\d+' )
87
    _macMatchRegex = re.compile( r'..:..:..:..:..:..' )
88

    
89
    def updateIP( self ):
90
        "Return updated IP address based on ifconfig"
91
        # use pexec instead of node.cmd so that we dont read
92
        # backgrounded output from the cli.
93
        ifconfig, _err, _exitCode = self.node.pexec( 'ifconfig %s' % self.name )
94
        ips = self._ipMatchRegex.findall( ifconfig )
95
        self.ip = ips[ 0 ] if ips else None
96
        return self.ip
97

    
98
    def updateMAC( self ):
99
        "Return updated MAC address based on ifconfig"
100
        ifconfig = self.ifconfig()
101
        macs = self._macMatchRegex.findall( ifconfig )
102
        self.mac = macs[ 0 ] if macs else None
103
        return self.mac
104

    
105
    # Instead of updating ip and mac separately,
106
    # use one ifconfig call to do it simultaneously.
107
    # This saves an ifconfig command, which improves performance.
108

    
109
    def updateAddr( self ):
110
        "Return IP address and MAC address based on ifconfig."
111
        ifconfig = self.ifconfig()
112
        ips = self._ipMatchRegex.findall( ifconfig )
113
        macs = self._macMatchRegex.findall( ifconfig )
114
        self.ip = ips[ 0 ] if ips else None
115
        self.mac = macs[ 0 ] if macs else None
116
        return self.ip, self.mac
117

    
118
    def IP( self ):
119
        "Return IP address"
120
        return self.ip
121

    
122
    def MAC( self ):
123
        "Return MAC address"
124
        return self.mac
125

    
126
    def isUp( self, setUp=False ):
127
        "Return whether interface is up"
128
        if setUp:
129
            cmdOutput = self.ifconfig( 'up' )
130
            # no output indicates success
131
            if cmdOutput:
132
                error( "Error setting %s up: %s " % ( self.name, cmdOutput ) )
133
                return False
134
            else:
135
                return True
136
        else:
137
            return "UP" in self.ifconfig()
138

    
139
    def rename( self, newname ):
140
        "Rename interface"
141
        self.ifconfig( 'down' )
142
        result = self.cmd( 'ip link set', self.name, 'name', newname )
143
        self.name = newname
144
        self.ifconfig( 'up' )
145
        return result
146

    
147
    # The reason why we configure things in this way is so
148
    # That the parameters can be listed and documented in
149
    # the config method.
150
    # Dealing with subclasses and superclasses is slightly
151
    # annoying, but at least the information is there!
152

    
153
    def setParam( self, results, method, **param ):
154
        """Internal method: configure a *single* parameter
155
           results: dict of results to update
156
           method: config method name
157
           param: arg=value (ignore if value=None)
158
           value may also be list or dict"""
159
        name, value = param.items()[ 0 ]
160
        f = getattr( self, method, None )
161
        if not f or value is None:
162
            return
163
        if isinstance( value, list ):
164
            result = f( *value )
165
        elif isinstance( value, dict ):
166
            result = f( **value )
167
        else:
168
            result = f( value )
169
        results[ name ] = result
170
        return result
171

    
172
    def config( self, mac=None, ip=None, ifconfig=None,
173
                up=True, **_params ):
174
        """Configure Node according to (optional) parameters:
175
           mac: MAC address
176
           ip: IP address
177
           ifconfig: arbitrary interface configuration
178
           Subclasses should override this method and call
179
           the parent class's config(**params)"""
180
        # If we were overriding this method, we would call
181
        # the superclass config method here as follows:
182
        # r = Parent.config( **params )
183
        r = {}
184
        self.setParam( r, 'setMAC', mac=mac )
185
        self.setParam( r, 'setIP', ip=ip )
186
        self.setParam( r, 'isUp', up=up )
187
        self.setParam( r, 'ifconfig', ifconfig=ifconfig )
188
        return r
189

    
190
    def delete( self ):
191
        "Delete interface"
192
        self.cmd( 'ip link del ' + self.name )
193
        if self.node.inNamespace:
194
            # Link may have been dumped into root NS
195
            quietRun( 'ip link del ' + self.name )
196

    
197
    def status( self ):
198
        "Return intf status as a string"
199
        links, err_, result_ = self.node.pexec( 'ip link show' )
200
        if self.name in links:
201
            return "OK"
202
        else:
203
            return "MISSING"
204

    
205
    def __repr__( self ):
206
        return '<%s %s>' % ( self.__class__.__name__, self.name )
207

    
208
    def __str__( self ):
209
        return self.name
210

    
211

    
212
class TCIntf( Intf ):
213
    """Interface customized by tc (traffic control) utility
214
       Allows specification of bandwidth limits (various methods)
215
       as well as delay, loss and max queue length"""
216

    
217
    def bwCmds( self, bw=None, speedup=0, use_hfsc=False, use_tbf=False,
218
                latency_ms=None, enable_ecn=False, enable_red=False ):
219
        "Return tc commands to set bandwidth"
220

    
221
        cmds, parent = [], ' root '
222

    
223
        if bw and ( bw < 0 or bw > 1000 ):
224
            error( 'Bandwidth', bw, 'is outside range 0..1000 Mbps\n' )
225

    
226
        elif bw is not None:
227
            # BL: this seems a bit brittle...
228
            if ( speedup > 0 and
229
                 self.node.name[0:1] == 's' ):
230
                bw = speedup
231
            # This may not be correct - we should look more closely
232
            # at the semantics of burst (and cburst) to make sure we
233
            # are specifying the correct sizes. For now I have used
234
            # the same settings we had in the mininet-hifi code.
235
            if use_hfsc:
236
                cmds += [ '%s qdisc add dev %s root handle 5:0 hfsc default 1',
237
                          '%s class add dev %s parent 5:0 classid 5:1 hfsc sc '
238
                          + 'rate %fMbit ul rate %fMbit' % ( bw, bw ) ]
239
            elif use_tbf:
240
                if latency_ms is None:
241
                    latency_ms = 15 * 8 / bw
242
                cmds += [ '%s qdisc add dev %s root handle 5: tbf ' +
243
                          'rate %fMbit burst 15000 latency %fms' %
244
                          ( bw, latency_ms ) ]
245
            else:
246
                cmds += [ '%s qdisc add dev %s root handle 5:0 htb default 1',
247
                          '%s class add dev %s parent 5:0 classid 5:1 htb ' +
248
                          'rate %fMbit burst 15k' % bw ]
249
            parent = ' parent 5:1 '
250

    
251
            # ECN or RED
252
            if enable_ecn:
253
                cmds += [ '%s qdisc add dev %s' + parent +
254
                          'handle 6: red limit 1000000 ' +
255
                          'min 30000 max 35000 avpkt 1500 ' +
256
                          'burst 20 ' +
257
                          'bandwidth %fmbit probability 1 ecn' % bw ]
258
                parent = ' parent 6: '
259
            elif enable_red:
260
                cmds += [ '%s qdisc add dev %s' + parent +
261
                          'handle 6: red limit 1000000 ' +
262
                          'min 30000 max 35000 avpkt 1500 ' +
263
                          'burst 20 ' +
264
                          'bandwidth %fmbit probability 1' % bw ]
265
                parent = ' parent 6: '
266
        return cmds, parent
267

    
268
    @staticmethod
269
    def delayCmds( parent, delay=None, jitter=None,
270
                   loss=None, max_queue_size=None ):
271
        "Internal method: return tc commands for delay and loss"
272
        cmds = []
273
        if delay and delay < 0:
274
            error( 'Negative delay', delay, '\n' )
275
        elif jitter and jitter < 0:
276
            error( 'Negative jitter', jitter, '\n' )
277
        elif loss and ( loss < 0 or loss > 100 ):
278
            error( 'Bad loss percentage', loss, '%%\n' )
279
        else:
280
            # Delay/jitter/loss/max queue size
281
            netemargs = '%s%s%s%s' % (
282
                'delay %s ' % delay if delay is not None else '',
283
                '%s ' % jitter if jitter is not None else '',
284
                'loss %d ' % loss if loss is not None else '',
285
                'limit %d' % max_queue_size if max_queue_size is not None
286
                else '' )
287
            if netemargs:
288
                cmds = [ '%s qdisc add dev %s ' + parent +
289
                         ' handle 10: netem ' +
290
                         netemargs ]
291
                parent = ' parent 10:1 '
292
        return cmds, parent
293

    
294
    def tc( self, cmd, tc='tc' ):
295
        "Execute tc command for our interface"
296
        c = cmd % (tc, self)  # Add in tc command and our name
297
        debug(" *** executing command: %s\n" % c)
298
        return self.cmd( c )
299

    
300
    def config( self, bw=None, delay=None, jitter=None, loss=None,
301
                disable_gro=True, speedup=0, use_hfsc=False, use_tbf=False,
302
                latency_ms=None, enable_ecn=False, enable_red=False,
303
                max_queue_size=None, **params ):
304
        "Configure the port and set its properties."
305

    
306
        result = Intf.config( self, **params)
307

    
308
        # Disable GRO
309
        if disable_gro:
310
            self.cmd( 'ethtool -K %s gro off' % self )
311

    
312
        # Optimization: return if nothing else to configure
313
        # Question: what happens if we want to reset things?
314
        if ( bw is None and not delay and not loss
315
             and max_queue_size is None ):
316
            return
317

    
318
        # Clear existing configuration
319
        tcoutput = self.tc( '%s qdisc show dev %s' )
320
        if "priomap" not in tcoutput:
321
            cmds = [ '%s qdisc del dev %s root' ]
322
        else:
323
            cmds = []
324

    
325
        # Bandwidth limits via various methods
326
        bwcmds, parent = self.bwCmds( bw=bw, speedup=speedup,
327
                                      use_hfsc=use_hfsc, use_tbf=use_tbf,
328
                                      latency_ms=latency_ms,
329
                                      enable_ecn=enable_ecn,
330
                                      enable_red=enable_red )
331
        cmds += bwcmds
332

    
333
        # Delay/jitter/loss/max_queue_size using netem
334
        delaycmds, parent = self.delayCmds( delay=delay, jitter=jitter,
335
                                loss=loss, max_queue_size=max_queue_size,
336
                                parent=parent )
337
        cmds += delaycmds
338

    
339
        # Ugly but functional: display configuration info
340
        stuff = ( ( [ '%.2fMbit' % bw ] if bw is not None else [] ) +
341
                  ( [ '%s delay' % delay ] if delay is not None else [] ) +
342
                  ( [ '%s jitter' % jitter ] if jitter is not None else [] ) +
343
                  ( ['%d%% loss' % loss ] if loss is not None else [] ) +
344
                  ( [ 'ECN' ] if enable_ecn else [ 'RED' ]
345
                    if enable_red else [] ) )
346
        info( '(' + ' '.join( stuff ) + ') ' )
347

    
348
        # Execute all the commands in our node
349
        debug("at map stage w/cmds: %s\n" % cmds)
350
        tcoutputs = [ self.tc(cmd) for cmd in cmds ]
351
        for output in tcoutputs:
352
            if output != '':
353
                error( "*** Error: %s" % output )
354
        debug( "cmds:", cmds, '\n' )
355
        debug( "outputs:", tcoutputs, '\n' )
356
        result[ 'tcoutputs'] = tcoutputs
357
        result[ 'parent' ] = parent
358

    
359
        return result
360

    
361

    
362
class Link( object ):
363

    
364
    """A basic link is just a veth pair.
365
       Other types of links could be tunnels, link emulators, etc.."""
366

    
367
    def __init__( self, node1, node2, port1=None, port2=None,
368
                  intfName1=None, intfName2=None, addr1=None, addr2=None,
369
                  intf=Intf, cls1=None, cls2=None, params1=None,
370
                  params2=None ):
371
        """Create veth link to another node, making two new interfaces.
372
           node1: first node
373
           node2: second node
374
           port1: node1 port number (optional)
375
           port2: node2 port number (optional)
376
           intf: default interface class/constructor
377
           cls1, cls2: optional interface-specific constructors
378
           intfName1: node1 interface name (optional)
379
           intfName2: node2  interface name (optional)
380
           params1: parameters for interface 1
381
           params2: parameters for interface 2"""
382
        # This is a bit awkward; it seems that having everything in
383
        # params is more orthogonal, but being able to specify
384
        # in-line arguments is more convenient! So we support both.
385
        if params1 is None:
386
            params1 = {}
387
        if params2 is None:
388
            params2 = {}
389
        # Allow passing in params1=params2
390
        if params2 is params1:
391
            params2 = dict( params1 )
392
        if port1 is not None:
393
            params1[ 'port' ] = port1
394
        if port2 is not None:
395
            params2[ 'port' ] = port2
396
        if 'port' not in params1:
397
            params1[ 'port' ] = node1.newPort()
398
        if 'port' not in params2:
399
            params2[ 'port' ] = node2.newPort()
400
        if not intfName1:
401
            intfName1 = self.intfName( node1, params1[ 'port' ] )
402
        if not intfName2:
403
            intfName2 = self.intfName( node2, params2[ 'port' ] )
404

    
405
        self.makeIntfPair( intfName1, intfName2, addr1, addr2 )
406

    
407
        if not cls1:
408
            cls1 = intf
409
        if not cls2:
410
            cls2 = intf
411

    
412
        intf1 = cls1( name=intfName1, node=node1,
413
                      link=self, mac=addr1, **params1  )
414
        intf2 = cls2( name=intfName2, node=node2,
415
                      link=self, mac=addr2, **params2 )
416

    
417
        # All we are is dust in the wind, and our two interfaces
418
        self.intf1, self.intf2 = intf1, intf2
419

    
420
    def intfName( self, node, n ):
421
        "Construct a canonical interface name node-ethN for interface n."
422
        return node.name + '-eth' + repr( n )
423

    
424
    @classmethod
425
    def makeIntfPair( _cls, intfname1, intfname2, addr1=None, addr2=None ):
426
        """Create pair of interfaces
427
           intfname1: name of interface 1
428
           intfname2: name of interface 2
429
           (override this method [and possibly delete()]
430
           to change link type)"""
431
        return makeIntfPair( intfname1, intfname2, addr1, addr2 )
432

    
433
    def delete( self ):
434
        "Delete this link"
435
        self.intf1.delete()
436
        self.intf2.delete()
437

    
438
    def stop( self ):
439
        "Override to stop and clean up link as needed"
440
        pass
441

    
442
    def status( self ):
443
        "Return link status as a string"
444
        return "(%s %s)" % ( self.intf1.status(), self.intf2.status() )
445

    
446
    def __str__( self ):
447
        return '%s<->%s' % ( self.intf1, self.intf2 )
448

    
449
class TCLink( Link ):
450
    "Link with symmetric TC interfaces configured via opts"
451
    def __init__( self, node1, node2, port1=None, port2=None,
452
                  intfName1=None, intfName2=None,
453
                  addr1=None, addr2=None, **params ):
454
        Link.__init__( self, node1, node2, port1=port1, port2=port2,
455
                       intfName1=intfName1, intfName2=intfName2,
456
                       cls1=TCIntf,
457
                       cls2=TCIntf,
458
                       addr1=addr1, addr2=addr2,
459
                       params1=params,
460
                       params2=params )