Statistics
| Branch: | Tag: | Revision:

mininet / mininet / link.py @ b1ec912d

History | View | Annotate | Download (17.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 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, **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'
75
                                 % ( ipstr, ) )
76
            self.ip, self.prefixLen = ipstr, prefixLen
77
            return self.ifconfig( '%s/%s' % ( ipstr, prefixLen ) )
78

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
212

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
360
        return result
361

    
362

    
363
class Link( object ):
364

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

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

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

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

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

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

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

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

    
438
    def delete( self ):
439
        "Delete this link"
440
        self.intf1.delete()
441
        self.intf2.delete()
442

    
443
    def stop( self ):
444
        "Override to stop and clean up link as needed"
445
        pass
446

    
447
    def status( self ):
448
        "Return link status as a string"
449
        return "(%s %s)" % ( self.intf1.status(), self.intf2.status() )
450

    
451
    def __str__( self ):
452
        return '%s<->%s' % ( self.intf1, self.intf2 )
453

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