Statistics
| Branch: | Tag: | Revision:

mininet / mininet / link.py @ c916f3ee

History | View | Annotate | Download (18.3 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
        node.addIntf( self, port=port )
54
        # Save params for future reference
55
        self.params = params
56
        self.config( **params )
57

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

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

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

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

    
88
    _ipMatchRegex = re.compile( r'\d+\.\d+\.\d+\.\d+' )
89
    _macMatchRegex = re.compile( r'..:..:..:..:..:..' )
90

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

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

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

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

    
121
    def IP( self ):
122
        "Return IP address"
123
        return self.ip
124

    
125
    def MAC( self ):
126
        "Return MAC address"
127
        return self.mac
128

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

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

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

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

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

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

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

    
208
    def __repr__( self ):
209
        return '<%s %s>' % ( self.__class__.__name__, self.name )
210

    
211
    def __str__( self ):
212
        return self.name
213

    
214

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

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

    
224
        cmds, parent = [], ' root '
225

    
226
        if bw and ( bw < 0 or bw > 1000 ):
227
            error( 'Bandwidth', bw, 'is outside range 0..1000 Mbps\n' )
228

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

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

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

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

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

    
309
        result = Intf.config( self, **params)
310

    
311
        # Disable GRO
312
        if disable_gro:
313
            self.cmd( 'ethtool -K %s gro off' % self )
314

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

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

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

    
336
        # Delay/jitter/loss/max_queue_size using netem
337
        delaycmds, parent = self.delayCmds( delay=delay, jitter=jitter,
338
                                            loss=loss,
339
                                            max_queue_size=max_queue_size,
340
                                            parent=parent )
341
        cmds += delaycmds
342

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

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

    
363
        return result
364

    
365

    
366
class Link( object ):
367

    
368
    """A basic link is just a veth pair.
369
       Other types of links could be tunnels, link emulators, etc.."""
370

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

    
409
        self.makeIntfPair( intfName1, intfName2, addr1, addr2 )
410

    
411
        if not cls1:
412
            cls1 = intf
413
        if not cls2:
414
            cls2 = intf
415

    
416
        intf1 = cls1( name=intfName1, node=node1,
417
                      link=self, mac=addr1, **params1  )
418
        intf2 = cls2( name=intfName2, node=node2,
419
                      link=self, mac=addr2, **params2 )
420

    
421
        # All we are is dust in the wind, and our two interfaces
422
        self.intf1, self.intf2 = intf1, intf2
423

    
424
    def intfName( self, node, n ):
425
        "Construct a canonical interface name node-ethN for interface n."
426
        # Leave this as an instance method for now
427
        assert self
428
        return node.name + '-eth' + repr( n )
429

    
430
    @classmethod
431
    def makeIntfPair( cls, intfname1, intfname2, addr1=None, addr2=None ):
432
        """Create pair of interfaces
433
           intfname1: name of interface 1
434
           intfname2: name of interface 2
435
           (override this method [and possibly delete()]
436
           to change link type)"""
437
        # Leave this as a class method for now
438
        assert cls
439
        return makeIntfPair( intfname1, intfname2, addr1, addr2 )
440

    
441
    def delete( self ):
442
        "Delete this link"
443
        self.intf1.delete()
444
        self.intf2.delete()
445

    
446
    def stop( self ):
447
        "Override to stop and clean up link as needed"
448
        pass
449

    
450
    def status( self ):
451
        "Return link status as a string"
452
        return "(%s %s)" % ( self.intf1.status(), self.intf2.status() )
453

    
454
    def __str__( self ):
455
        return '%s<->%s' % ( self.intf1, self.intf2 )
456

    
457

    
458
class OVSIntf( Intf ):
459
    "Patch interface on an OVSSwitch"
460

    
461
    def ifconfig( self, cmd ):
462
        if cmd == 'up':
463
            "OVSIntf is always up"
464
            return
465
        else:
466
            raise Exception( 'OVSIntf cannot do ifconfig ' + cmd )
467

    
468

    
469
class OVSLink( Link ):
470
    "Link that makes patch links between OVSSwitches"
471

    
472
    def __init__( self, node1, node2, **kwargs ):
473
        "See Link.__init__() for options"
474
        self.isPatchLink = False
475
        if ( type( node1 ) is mininet.node.OVSSwitch and
476
             type( node2 ) is mininet.node.OVSSwitch ):
477
             self.isPatchLink = True
478
             kwargs.update( cls1=OVSIntf, cls2=OVSIntf )
479
        Link.__init__( self, node1, node2, **kwargs )
480

    
481
    def makeIntfPair( self, *args, **kwargs ):
482
        "Usually delegated to OVSSwitch"
483
        if self.isPatchLink:
484
            return None, None
485
        else:
486
            return Link.makeIntfPair( *args, **kwargs )
487

    
488

    
489
class TCLink( Link ):
490
    "Link with symmetric TC interfaces configured via opts"
491
    def __init__( self, node1, node2, port1=None, port2=None,
492
                  intfName1=None, intfName2=None,
493
                  addr1=None, addr2=None, **params ):
494
        Link.__init__( self, node1, node2, port1=port1, port2=port2,
495
                       intfName1=intfName1, intfName2=intfName2,
496
                       cls1=TCIntf,
497
                       cls2=TCIntf,
498
                       addr1=addr1, addr2=addr2,
499
                       params1=params,
500
                       params2=params )