Statistics
| Branch: | Tag: | Revision:

mininet / mininet / link.py @ 9db6cdc2

History | View | Annotate | Download (19.6 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
        if self.node.inNamespace:
201
            # Link may have been dumped into root NS
202
            quietRun( 'ip link del ' + self.name )
203

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

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

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

    
218

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

    
224
    def bwCmds( self, bw=None, speedup=0, use_hfsc=False, use_tbf=False,
225
                latency_ms=None, enable_ecn=False, enable_red=False ):
226
        "Return tc commands to set bandwidth"
227

    
228
        cmds, parent = [], ' root '
229

    
230
        if bw and ( bw < 0 or bw > 1000 ):
231
            error( 'Bandwidth', bw, 'is outside range 0..1000 Mbps\n' )
232

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

    
258
            # ECN or RED
259
            if enable_ecn:
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 ecn' % bw ]
265
                parent = ' parent 6: '
266
            elif enable_red:
267
                cmds += [ '%s qdisc add dev %s' + parent +
268
                          'handle 6: red limit 1000000 ' +
269
                          'min 30000 max 35000 avpkt 1500 ' +
270
                          'burst 20 ' +
271
                          'bandwidth %fmbit probability 1' % bw ]
272
                parent = ' parent 6: '
273
        return cmds, parent
274

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

    
301
    def tc( self, cmd, tc='tc' ):
302
        "Execute tc command for our interface"
303
        c = cmd % (tc, self)  # Add in tc command and our name
304
        debug(" *** executing command: %s\n" % c)
305
        return self.cmd( c )
306

    
307
    def config( self, bw=None, delay=None, jitter=None, loss=None,
308
                disable_gro=True, speedup=0, use_hfsc=False, use_tbf=False,
309
                latency_ms=None, enable_ecn=False, enable_red=False,
310
                max_queue_size=None, **params ):
311
        "Configure the port and set its properties."
312

    
313
        result = Intf.config( self, **params)
314

    
315
        # Disable GRO
316
        if disable_gro:
317
            self.cmd( 'ethtool -K %s gro off' % self )
318

    
319
        # Optimization: return if nothing else to configure
320
        # Question: what happens if we want to reset things?
321
        if ( bw is None and not delay and not loss
322
             and max_queue_size is None ):
323
            return
324

    
325
        # Clear existing configuration
326
        tcoutput = self.tc( '%s qdisc show dev %s' )
327
        if "priomap" not in tcoutput:
328
            cmds = [ '%s qdisc del dev %s root' ]
329
        else:
330
            cmds = []
331

    
332
        # Bandwidth limits via various methods
333
        bwcmds, parent = self.bwCmds( bw=bw, speedup=speedup,
334
                                      use_hfsc=use_hfsc, use_tbf=use_tbf,
335
                                      latency_ms=latency_ms,
336
                                      enable_ecn=enable_ecn,
337
                                      enable_red=enable_red )
338
        cmds += bwcmds
339

    
340
        # Delay/jitter/loss/max_queue_size using netem
341
        delaycmds, parent = self.delayCmds( delay=delay, jitter=jitter,
342
                                            loss=loss,
343
                                            max_queue_size=max_queue_size,
344
                                            parent=parent )
345
        cmds += delaycmds
346

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

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

    
367
        return result
368

    
369

    
370
class Link( object ):
371

    
372
    """A basic link is just a veth pair.
373
       Other types of links could be tunnels, link emulators, etc.."""
374

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

    
414
        self.fast = fast
415
        if fast:
416
            params1.setdefault( 'moveIntfFn', self._ignore )
417
            params2.setdefault( 'moveIntfFn', self._ignore )
418
            self.makeIntfPair( intfName1, intfName2, addr1, addr2,
419
                               node1, node2, deleteIntfs=False )
420
        else:
421
            self.makeIntfPair( intfName1, intfName2, addr1, addr2 )
422

    
423
        if not cls1:
424
            cls1 = intf
425
        if not cls2:
426
            cls2 = intf
427
        # pylint: enable=too-many-branches
428

    
429
        intf1 = cls1( name=intfName1, node=node1,
430
                      link=self, mac=addr1, **params1  )
431
        intf2 = cls2( name=intfName2, node=node2,
432
                      link=self, mac=addr2, **params2 )
433

    
434
        # All we are is dust in the wind, and our two interfaces
435
        self.intf1, self.intf2 = intf1, intf2
436

    
437
    @staticmethod
438
    def _ignore( *args, **kwargs ):
439
        "Ignore any arguments"
440
        pass
441

    
442
    def intfName( self, node, n ):
443
        "Construct a canonical interface name node-ethN for interface n."
444
        # Leave this as an instance method for now
445
        assert self
446
        return node.name + '-eth' + repr( n )
447

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

    
465
    def delete( self ):
466
        "Delete this link"
467
        self.intf1.delete()
468
        # We only need to delete one side, though this doesn't seem to
469
        # cost us much and might help subclasses.
470
        # self.intf2.delete()
471

    
472
    def stop( self ):
473
        "Override to stop and clean up link as needed"
474
        self.delete()
475

    
476
    def status( self ):
477
        "Return link status as a string"
478
        return "(%s %s)" % ( self.intf1.status(), self.intf2.status() )
479

    
480
    def __str__( self ):
481
        return '%s<->%s' % ( self.intf1, self.intf2 )
482

    
483

    
484
class OVSIntf( Intf ):
485
    "Patch interface on an OVSSwitch"
486

    
487
    def ifconfig( self, *args ):
488
        cmd = ' '.join( args )
489
        if cmd == 'up':
490
            # OVSIntf is always up
491
            return
492
        else:
493
            raise Exception( 'OVSIntf cannot do ifconfig ' + cmd )
494

    
495

    
496
class OVSLink( Link ):
497
    """Link that makes patch links between OVSSwitches
498
       Warning: in testing we have found that no more
499
       than ~64 OVS patch links should be used in row."""
500

    
501
    def __init__( self, node1, node2, **kwargs ):
502
        "See Link.__init__() for options"
503
        self.isPatchLink = False
504
        if ( isinstance( node1, mininet.node.OVSSwitch ) and
505
             isinstance( node2, mininet.node.OVSSwitch ) ):
506
            self.isPatchLink = True
507
            kwargs.update( cls1=OVSIntf, cls2=OVSIntf )
508
        Link.__init__( self, node1, node2, **kwargs )
509

    
510
    def makeIntfPair( self, *args, **kwargs ):
511
        "Usually delegated to OVSSwitch"
512
        if self.isPatchLink:
513
            return None, None
514
        else:
515
            return Link.makeIntfPair( *args, **kwargs )
516

    
517

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