Statistics
| Branch: | Tag: | Revision:

mininet / mininet / link.py @ 2e3258d2

History | View | Annotate | Download (20.5 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
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
        moveIntfFn = params.pop( 'moveIntfFn', None )
54
        if moveIntfFn:
55
            node.addIntf( self, port=port, moveIntfFn=moveIntfFn )
56
        else:
57
            node.addIntf( self, port=port )
58
        # Save params for future reference
59
        self.params = params
60
        self.config( **params )
61

    
62
    def cmd( self, *args, **kwargs ):
63
        "Run a command in our owning node"
64
        return self.node.cmd( *args, **kwargs )
65

    
66
    def ifconfig( self, *args ):
67
        "Configure ourselves using ifconfig"
68
        return self.cmd( 'ifconfig', self.name, *args )
69

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

    
84
    def setMAC( self, macstr ):
85
        """Set the MAC address for an interface.
86
           macstr: MAC address as string"""
87
        self.mac = macstr
88
        return ( self.ifconfig( 'down' ) +
89
                 self.ifconfig( 'hw', 'ether', macstr ) +
90
                 self.ifconfig( 'up' ) )
91

    
92
    _ipMatchRegex = re.compile( r'\d+\.\d+\.\d+\.\d+' )
93
    _macMatchRegex = re.compile( r'..:..:..:..:..:..' )
94

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

    
105
    def updateMAC( self ):
106
        "Return updated MAC address based on ifconfig"
107
        ifconfig = self.ifconfig()
108
        macs = self._macMatchRegex.findall( ifconfig )
109
        self.mac = macs[ 0 ] if macs else None
110
        return self.mac
111

    
112
    # Instead of updating ip and mac separately,
113
    # use one ifconfig call to do it simultaneously.
114
    # This saves an ifconfig command, which improves performance.
115

    
116
    def updateAddr( self ):
117
        "Return IP address and MAC address based on ifconfig."
118
        ifconfig = self.ifconfig()
119
        ips = self._ipMatchRegex.findall( ifconfig )
120
        macs = self._macMatchRegex.findall( ifconfig )
121
        self.ip = ips[ 0 ] if ips else None
122
        self.mac = macs[ 0 ] if macs else None
123
        return self.ip, self.mac
124

    
125
    def IP( self ):
126
        "Return IP address"
127
        return self.ip
128

    
129
    def MAC( self ):
130
        "Return MAC address"
131
        return self.mac
132

    
133
    def isUp( self, setUp=False ):
134
        "Return whether interface is up"
135
        if setUp:
136
            cmdOutput = self.ifconfig( 'up' )
137
            # no output indicates success
138
            if cmdOutput:
139
                error( "Error setting %s up: %s " % ( self.name, cmdOutput ) )
140
                return False
141
            else:
142
                return True
143
        else:
144
            return "UP" in self.ifconfig()
145

    
146
    def rename( self, newname ):
147
        "Rename interface"
148
        self.ifconfig( 'down' )
149
        result = self.cmd( 'ip link set', self.name, 'name', newname )
150
        self.name = newname
151
        self.ifconfig( 'up' )
152
        return result
153

    
154
    # The reason why we configure things in this way is so
155
    # That the parameters can be listed and documented in
156
    # the config method.
157
    # Dealing with subclasses and superclasses is slightly
158
    # annoying, but at least the information is there!
159

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

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

    
197
    def delete( self ):
198
        "Delete interface"
199
        self.cmd( 'ip link del ' + self.name )
200
        # We used to do this, but it slows us down:
201
        # if self.node.inNamespace:
202
        # Link may have been dumped into root NS
203
        # quietRun( 'ip link del ' + self.name )
204

    
205
    def status( self ):
206
        "Return intf status as a string"
207
        links, _err, _result = self.node.pexec( 'ip link show' )
208
        if self.name in links:
209
            return "OK"
210
        else:
211
            return "MISSING"
212

    
213
    def __repr__( self ):
214
        return '<%s %s>' % ( self.__class__.__name__, self.name )
215

    
216
    def __str__( self ):
217
        return self.name
218

    
219

    
220
class TCIntf( Intf ):
221
    """Interface customized by tc (traffic control) utility
222
       Allows specification of bandwidth limits (various methods)
223
       as well as delay, loss and max queue length"""
224

    
225
    # The parameters we use seem to work reasonably up to 1 Gb/sec
226
    # For higher data rates, we will probably need to change them.
227
    bwParamMax = 1000
228

    
229
    def __init__( self, name, node=None, port=None, link=None, mac=None, **params ):
230
        super(TCIntf,self).__init__(name, node, port, link, mac, **params)
231
        self.params = params
232
        self.config( **params )
233

    
234
    def bwCmds( self, bw=None, speedup=0, use_hfsc=False, use_tbf=False,
235
                latency_ms=None, enable_ecn=False, enable_red=False ):
236
        "Return tc commands to set bandwidth"
237

    
238
        cmds, parent = [], ' root '
239

    
240
        if bw and ( bw < 0 or bw > self.bwParamMax ):
241
            error( 'Bandwidth limit', bw, 'is outside supported range 0..%d'
242
                   % self.bwParamMax, '- ignoring\n' )
243
        elif bw is not None:
244
            # BL: this seems a bit brittle...
245
            if ( speedup > 0 and
246
                 self.node.name[0:1] == 's' ):
247
                bw = speedup
248
            # This may not be correct - we should look more closely
249
            # at the semantics of burst (and cburst) to make sure we
250
            # are specifying the correct sizes. For now I have used
251
            # the same settings we had in the mininet-hifi code.
252
            if use_hfsc:
253
                cmds += [ '%s qdisc add dev %s root handle 5:0 hfsc default 1',
254
                          '%s class add dev %s parent 5:0 classid 5:1 hfsc sc '
255
                          + 'rate %fMbit ul rate %fMbit' % ( bw, bw ) ]
256
            elif use_tbf:
257
                if latency_ms is None:
258
                    latency_ms = 15 * 8 / bw
259
                cmds += [ '%s qdisc add dev %s root handle 5: tbf ' +
260
                          'rate %fMbit burst 15000 latency %fms' %
261
                          ( bw, latency_ms ) ]
262
            else:
263
                cmds += [ '%s qdisc add dev %s root handle 5:0 htb default 1',
264
                          '%s class add dev %s parent 5:0 classid 5:1 htb ' +
265
                          'rate %fMbit burst 15k' % bw ]
266
            parent = ' parent 5:1 '
267

    
268
            # ECN or RED
269
            if enable_ecn:
270
                cmds += [ '%s qdisc add dev %s' + parent +
271
                          'handle 6: red limit 1000000 ' +
272
                          'min 30000 max 35000 avpkt 1500 ' +
273
                          'burst 20 ' +
274
                          'bandwidth %fmbit probability 1 ecn' % bw ]
275
                parent = ' parent 6: '
276
            elif enable_red:
277
                cmds += [ '%s qdisc add dev %s' + parent +
278
                          'handle 6: red limit 1000000 ' +
279
                          'min 30000 max 35000 avpkt 1500 ' +
280
                          'burst 20 ' +
281
                          'bandwidth %fmbit probability 1' % bw ]
282
                parent = ' parent 6: '
283
        return cmds, parent
284

    
285
    @staticmethod
286
    def delayCmds( parent, delay=None, jitter=None, delay_distribution=None,
287
                   loss=None, max_queue_size=None ):
288
        "Internal method: return tc commands for delay and loss"
289
        cmds = []
290
        if delay and delay < 0:
291
            error( 'Negative delay', delay, '\n' )
292
        elif jitter and jitter < 0:
293
            error( 'Negative jitter', jitter, '\n' )
294
        elif loss and ( loss < 0 or loss > 100 ):
295
            error( 'Bad loss percentage', loss, '%%\n' )
296
        else:
297
            # Delay/jitter/loss/max queue size
298
            netemargs = '%s%s%s%s%s' % (
299
                'delay %s ' % delay if delay is not None else '',
300
                '%s ' % jitter if jitter is not None else '',
301
                'distribution %s ' % delay_distribution if delay_distribution is not None else '',
302
                'loss %d ' % loss if loss is not None else '',
303
                'limit %d' % max_queue_size if max_queue_size is not None
304
                else '' )
305
            if netemargs:
306
                cmds = [ '%s qdisc add dev %s ' + parent +
307
                         ' handle 10: netem ' +
308
                         netemargs ]
309
                parent = ' parent 10:1 '
310
        return cmds, parent
311

    
312
    def tc( self, cmd, tc='tc' ):
313
        "Execute tc command for our interface"
314
        c = cmd % (tc, self)  # Add in tc command and our name
315
        debug(" *** executing command: %s\n" % c)
316
        return self.cmd( c )
317

    
318
    def config( self, bw=None, delay=None, jitter=None, loss=None, delay_distribution=None,
319
                disable_gro=True, speedup=0, use_hfsc=False, use_tbf=False,
320
                latency_ms=None, enable_ecn=False, enable_red=False,
321
                max_queue_size=None, **params ):
322
        "Configure the port and set its properties."
323

    
324
        result = super(TCIntf, self).config( self, **params)
325

    
326
        # Disable GRO
327
        if disable_gro:
328
            self.cmd( 'ethtool -K %s gro off' % self )
329

    
330
        # Optimization: return if nothing else to configure
331
        # Question: what happens if we want to reset things?
332
        if ( bw is None and not delay and not loss
333
             and max_queue_size is None ):
334
            return
335

    
336
        # Clear existing configuration
337
        tcoutput = self.tc( '%s qdisc show dev %s' )
338
        if "priomap" not in tcoutput:
339
            cmds = [ '%s qdisc del dev %s root' ]
340
        else:
341
            cmds = []
342

    
343
        # Bandwidth limits via various methods
344
        bwcmds, parent = self.bwCmds( bw=bw, speedup=speedup,
345
                                      use_hfsc=use_hfsc, use_tbf=use_tbf,
346
                                      latency_ms=latency_ms,
347
                                      enable_ecn=enable_ecn,
348
                                      enable_red=enable_red )
349
        cmds += bwcmds
350

    
351
        # Delay/jitter/loss/max_queue_size using netem
352
        delaycmds, parent = self.delayCmds( delay=delay, jitter=jitter,
353
                                            delay_distribution=delay_distribution,
354
                                            loss=loss,
355
                                            max_queue_size=max_queue_size,
356
                                            parent=parent )
357
        cmds += delaycmds
358

    
359
        # Ugly but functional: display configuration info
360
        stuff = ( ( [ '%.2fMbit' % bw ] if bw is not None else [] ) +
361
                  ( [ '%s delay' % delay ] if delay is not None else [] ) +
362
                  ( [ '%s jitter' % jitter ] if jitter is not None else [] ) +
363
                  ( ['%d%% loss' % loss ] if loss is not None else [] ) +
364
                  ( [ 'ECN' ] if enable_ecn else [ 'RED' ]
365
                    if enable_red else [] ) )
366
        info( '(' + ' '.join( stuff ) + ') ' )
367

    
368
        # Execute all the commands in our node
369
        debug("at map stage w/cmds: %s\n" % cmds)
370
        tcoutputs = [ self.tc(cmd) for cmd in cmds ]
371
        for output in tcoutputs:
372
            if output != '':
373
                error( "*** Error: %s" % output )
374
        debug( "cmds:", cmds, '\n' )
375
        debug( "outputs:", tcoutputs, '\n' )
376
        result[ 'tcoutputs'] = tcoutputs
377
        result[ 'parent' ] = parent
378

    
379
        return result
380

    
381

    
382
class Link( object ):
383

    
384
    """A basic link is just a veth pair.
385
       Other types of links could be tunnels, link emulators, etc.."""
386

    
387
    # pylint: disable=too-many-branches
388
    def __init__( self, node1, node2, port1=None, port2=None,
389
                  intfName1=None, intfName2=None, addr1=None, addr2=None,
390
                  intf=Intf, cls1=None, cls2=None, params1=None,
391
                  params2=None, fast=True ):
392
        """Create veth link to another node, making two new interfaces.
393
           node1: first node
394
           node2: second node
395
           port1: node1 port number (optional)
396
           port2: node2 port number (optional)
397
           intf: default interface class/constructor
398
           cls1, cls2: optional interface-specific constructors
399
           intfName1: node1 interface name (optional)
400
           intfName2: node2  interface name (optional)
401
           params1: parameters for interface 1
402
           params2: parameters for interface 2"""
403
        # This is a bit awkward; it seems that having everything in
404
        # params is more orthogonal, but being able to specify
405
        # in-line arguments is more convenient! So we support both.
406
        if params1 is None:
407
            params1 = {}
408
        if params2 is None:
409
            params2 = {}
410
        # Allow passing in params1=params2
411
        if params2 is params1:
412
            params2 = dict( params1 )
413
        if port1 is not None:
414
            params1[ 'port' ] = port1
415
        if port2 is not None:
416
            params2[ 'port' ] = port2
417
        if 'port' not in params1:
418
            params1[ 'port' ] = node1.newPort()
419
        if 'port' not in params2:
420
            params2[ 'port' ] = node2.newPort()
421
        if not intfName1:
422
            intfName1 = self.intfName( node1, params1[ 'port' ] )
423
        if not intfName2:
424
            intfName2 = self.intfName( node2, params2[ 'port' ] )
425

    
426
        self.fast = fast
427
        if fast:
428
            params1.setdefault( 'moveIntfFn', self._ignore )
429
            params2.setdefault( 'moveIntfFn', self._ignore )
430
            self.makeIntfPair( intfName1, intfName2, addr1, addr2,
431
                               node1, node2, deleteIntfs=False )
432
        else:
433
            self.makeIntfPair( intfName1, intfName2, addr1, addr2 )
434

    
435
        if not cls1:
436
            cls1 = intf
437
        if not cls2:
438
            cls2 = intf
439

    
440
        intf1 = cls1( name=intfName1, node=node1,
441
                      link=self, mac=addr1, **params1  )
442
        intf2 = cls2( name=intfName2, node=node2,
443
                      link=self, mac=addr2, **params2 )
444

    
445
        # All we are is dust in the wind, and our two interfaces
446
        self.intf1, self.intf2 = intf1, intf2
447
    # pylint: enable=too-many-branches
448

    
449
    @staticmethod
450
    def _ignore( *args, **kwargs ):
451
        "Ignore any arguments"
452
        pass
453

    
454
    def intfName( self, node, n ):
455
        "Construct a canonical interface name node-ethN for interface n."
456
        # Leave this as an instance method for now
457
        assert self
458
        return node.name + '-eth' + repr( n )
459

    
460
    @classmethod
461
    def makeIntfPair( cls, intfname1, intfname2, addr1=None, addr2=None,
462
                      node1=None, node2=None, deleteIntfs=True ):
463
        """Create pair of interfaces
464
           intfname1: name for interface 1
465
           intfname2: name for interface 2
466
           addr1: MAC address for interface 1 (optional)
467
           addr2: MAC address for interface 2 (optional)
468
           node1: home node for interface 1 (optional)
469
           node2: home node for interface 2 (optional)
470
           (override this method [and possibly delete()]
471
           to change link type)"""
472
        # Leave this as a class method for now
473
        assert cls
474
        return makeIntfPair( intfname1, intfname2, addr1, addr2, node1, node2,
475
                             deleteIntfs=deleteIntfs )
476

    
477
    def delete( self ):
478
        "Delete this link"
479
        self.intf1.delete()
480
        # We only need to delete one side, though this doesn't seem to
481
        # cost us much and might help subclasses.
482
        # self.intf2.delete()
483

    
484
    def stop( self ):
485
        "Override to stop and clean up link as needed"
486
        self.delete()
487

    
488
    def status( self ):
489
        "Return link status as a string"
490
        return "(%s %s)" % ( self.intf1.status(), self.intf2.status() )
491

    
492
    def __str__( self ):
493
        return '%s<->%s' % ( self.intf1, self.intf2 )
494

    
495

    
496
class OVSIntf( Intf ):
497
    "Patch interface on an OVSSwitch"
498

    
499
    def ifconfig( self, *args ):
500
        cmd = ' '.join( args )
501
        if cmd == 'up':
502
            # OVSIntf is always up
503
            return
504
        else:
505
            raise Exception( 'OVSIntf cannot do ifconfig ' + cmd )
506

    
507

    
508
class OVSLink( Link ):
509
    """Link that makes patch links between OVSSwitches
510
       Warning: in testing we have found that no more
511
       than ~64 OVS patch links should be used in row."""
512

    
513
    def __init__( self, node1, node2, **kwargs ):
514
        "See Link.__init__() for options"
515
        self.isPatchLink = False
516
        if ( isinstance( node1, mininet.node.OVSSwitch ) and
517
             isinstance( node2, mininet.node.OVSSwitch ) ):
518
            self.isPatchLink = True
519
            kwargs.update( cls1=OVSIntf, cls2=OVSIntf )
520
        Link.__init__( self, node1, node2, **kwargs )
521

    
522
    def makeIntfPair( self, *args, **kwargs ):
523
        "Usually delegated to OVSSwitch"
524
        if self.isPatchLink:
525
            return None, None
526
        else:
527
            return Link.makeIntfPair( *args, **kwargs )
528

    
529

    
530
class TCLink( Link ):
531
    "Link with symmetric TC interfaces configured via opts"
532
    def __init__( self, node1, node2, port1=None, port2=None,
533
                  intfName1=None, intfName2=None,
534
                  addr1=None, addr2=None, **params ):
535
        if 'params1' in params.keys():
536
            prms1 = params['params1']
537
        else:
538
            prms1 = None
539
        if 'params2' in params.keys():
540
            prms2 = params['params2']
541
        else:
542
            prms2 = None
543
        Link.__init__( self, node1, node2, port1=port1, port2=port2,
544
                       intfName1=intfName1, intfName2=intfName2,
545
                       cls1=TCIntf,
546
                       cls2=TCIntf,
547
                       addr1=addr1, addr2=addr2,
548
                       params1=prms1,
549
                       params2=prms2 )