Statistics
| Branch: | Tag: | Revision:

mininet / mininet / link.py @ 574d634f

History | View | Annotate | Download (19.9 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
        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 bwCmds( self, bw=None, speedup=0, use_hfsc=False, use_tbf=False,
230
                latency_ms=None, enable_ecn=False, enable_red=False ):
231
        "Return tc commands to set bandwidth"
232

    
233
        cmds, parent = [], ' root '
234

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

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

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

    
306
    def tc( self, cmd, tc='tc' ):
307
        "Execute tc command for our interface"
308
        c = cmd % (tc, self)  # Add in tc command and our name
309
        debug(" *** executing command: %s\n" % c)
310
        return self.cmd( c )
311

    
312
    def config( self, bw=None, delay=None, jitter=None, loss=None,
313
                disable_gro=True, speedup=0, use_hfsc=False, use_tbf=False,
314
                latency_ms=None, enable_ecn=False, enable_red=False,
315
                max_queue_size=None, **params ):
316
        "Configure the port and set its properties."
317

    
318
        result = Intf.config( self, **params)
319

    
320
        # Disable GRO
321
        if disable_gro:
322
            self.cmd( 'ethtool -K %s gro off' % self )
323

    
324
        # Optimization: return if nothing else to configure
325
        # Question: what happens if we want to reset things?
326
        if ( bw is None and not delay and not loss
327
             and max_queue_size is None ):
328
            return
329

    
330
        # Clear existing configuration
331
        tcoutput = self.tc( '%s qdisc show dev %s' )
332
        if "priomap" not in tcoutput:
333
            cmds = [ '%s qdisc del dev %s root' ]
334
        else:
335
            cmds = []
336

    
337
        # Bandwidth limits via various methods
338
        bwcmds, parent = self.bwCmds( bw=bw, speedup=speedup,
339
                                      use_hfsc=use_hfsc, use_tbf=use_tbf,
340
                                      latency_ms=latency_ms,
341
                                      enable_ecn=enable_ecn,
342
                                      enable_red=enable_red )
343
        cmds += bwcmds
344

    
345
        # Delay/jitter/loss/max_queue_size using netem
346
        delaycmds, parent = self.delayCmds( delay=delay, jitter=jitter,
347
                                            loss=loss,
348
                                            max_queue_size=max_queue_size,
349
                                            parent=parent )
350
        cmds += delaycmds
351

    
352
        # Ugly but functional: display configuration info
353
        stuff = ( ( [ '%.2fMbit' % bw ] if bw is not None else [] ) +
354
                  ( [ '%s delay' % delay ] if delay is not None else [] ) +
355
                  ( [ '%s jitter' % jitter ] if jitter is not None else [] ) +
356
                  ( ['%d%% loss' % loss ] if loss is not None else [] ) +
357
                  ( [ 'ECN' ] if enable_ecn else [ 'RED' ]
358
                    if enable_red else [] ) )
359
        info( '(' + ' '.join( stuff ) + ') ' )
360

    
361
        # Execute all the commands in our node
362
        debug("at map stage w/cmds: %s\n" % cmds)
363
        tcoutputs = [ self.tc(cmd) for cmd in cmds ]
364
        for output in tcoutputs:
365
            if output != '':
366
                error( "*** Error: %s" % output )
367
        debug( "cmds:", cmds, '\n' )
368
        debug( "outputs:", tcoutputs, '\n' )
369
        result[ 'tcoutputs'] = tcoutputs
370
        result[ 'parent' ] = parent
371

    
372
        return result
373

    
374

    
375
class Link( object ):
376

    
377
    """A basic link is just a veth pair.
378
       Other types of links could be tunnels, link emulators, etc.."""
379

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

    
419
        self.fast = fast
420
        if fast:
421
            params1.setdefault( 'moveIntfFn', self._ignore )
422
            params2.setdefault( 'moveIntfFn', self._ignore )
423
            self.makeIntfPair( intfName1, intfName2, addr1, addr2,
424
                               node1, node2, deleteIntfs=False )
425
        else:
426
            self.makeIntfPair( intfName1, intfName2, addr1, addr2 )
427

    
428
        if not cls1:
429
            cls1 = intf
430
        if not cls2:
431
            cls2 = intf
432
        # pylint: enable=too-many-branches
433

    
434
        intf1 = cls1( name=intfName1, node=node1,
435
                      link=self, mac=addr1, **params1  )
436
        intf2 = cls2( name=intfName2, node=node2,
437
                      link=self, mac=addr2, **params2 )
438

    
439
        # All we are is dust in the wind, and our two interfaces
440
        self.intf1, self.intf2 = intf1, intf2
441

    
442
    @staticmethod
443
    def _ignore( *args, **kwargs ):
444
        "Ignore any arguments"
445
        pass
446

    
447
    def intfName( self, node, n ):
448
        "Construct a canonical interface name node-ethN for interface n."
449
        # Leave this as an instance method for now
450
        assert self
451
        return node.name + '-eth' + repr( n )
452

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

    
470
    def delete( self ):
471
        "Delete this link"
472
        self.intf1.delete()
473
        # We only need to delete one side, though this doesn't seem to
474
        # cost us much and might help subclasses.
475
        # self.intf2.delete()
476

    
477
    def stop( self ):
478
        "Override to stop and clean up link as needed"
479
        self.delete()
480

    
481
    def status( self ):
482
        "Return link status as a string"
483
        return "(%s %s)" % ( self.intf1.status(), self.intf2.status() )
484

    
485
    def __str__( self ):
486
        return '%s<->%s' % ( self.intf1, self.intf2 )
487

    
488

    
489
class OVSIntf( Intf ):
490
    "Patch interface on an OVSSwitch"
491

    
492
    def ifconfig( self, *args ):
493
        cmd = ' '.join( args )
494
        if cmd == 'up':
495
            # OVSIntf is always up
496
            return
497
        else:
498
            raise Exception( 'OVSIntf cannot do ifconfig ' + cmd )
499

    
500

    
501
class OVSLink( Link ):
502
    """Link that makes patch links between OVSSwitches
503
       Warning: in testing we have found that no more
504
       than ~64 OVS patch links should be used in row."""
505

    
506
    def __init__( self, node1, node2, **kwargs ):
507
        "See Link.__init__() for options"
508
        self.isPatchLink = False
509
        if ( isinstance( node1, mininet.node.OVSSwitch ) and
510
             isinstance( node2, mininet.node.OVSSwitch ) ):
511
            self.isPatchLink = True
512
            kwargs.update( cls1=OVSIntf, cls2=OVSIntf )
513
        Link.__init__( self, node1, node2, **kwargs )
514

    
515
    def makeIntfPair( self, *args, **kwargs ):
516
        "Usually delegated to OVSSwitch"
517
        if self.isPatchLink:
518
            return None, None
519
        else:
520
            return Link.makeIntfPair( *args, **kwargs )
521

    
522

    
523
class TCLink( Link ):
524
    "Link with symmetric TC interfaces configured via opts"
525
    def __init__( self, node1, node2, port1=None, port2=None,
526
                  intfName1=None, intfName2=None,
527
                  addr1=None, addr2=None, **params ):
528
        Link.__init__( self, node1, node2, port1=port1, port2=port2,
529
                       intfName1=intfName1, intfName2=intfName2,
530
                       cls1=TCIntf,
531
                       cls2=TCIntf,
532
                       addr1=addr1, addr2=addr2,
533
                       params1=params,
534
                       params2=params )