Statistics
| Branch: | Tag: | Revision:

mininet / mininet / link.py @ 5d529edf

History | View | Annotate | Download (14.8 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, **params ):
36
        """name: interface name (e.g. h1-eth0)
37
           node: owning node (where this intf most likely lives)
38
           link: parent link if we're part of a link
39
           other arguments are passed to config()"""
40
        self.node = node
41
        self.name = name
42
        self.link = link
43
        self.mac, self.ip, self.prefixLen = None, None, None
44
        # Add to node (and move ourselves if necessary )
45
        node.addIntf( self, port=port )
46
        # Save params for future reference
47
        self.params = params
48
        self.config( **params )
49

    
50
    def cmd( self, *args, **kwargs ):
51
        "Run a command in our owning node"
52
        return self.node.cmd( *args, **kwargs )
53

    
54
    def ifconfig( self, *args ):
55
        "Configure ourselves using ifconfig"
56
        return self.cmd( 'ifconfig', self.name, *args )
57

    
58
    def setIP( self, ipstr, prefixLen=None ):
59
        """Set our IP address"""
60
        # This is a sign that we should perhaps rethink our prefix
61
        # mechanism and/or the way we specify IP addresses
62
        if '/' in ipstr:
63
            self.ip, self.prefixLen = ipstr.split( '/' )
64
            return self.ifconfig( ipstr, 'up' )
65
        else:
66
            self.ip, self.prefixLen = ipstr, prefixLen
67
            return self.ifconfig( '%s/%s' % ( ipstr, prefixLen ) )
68

    
69
    def setMAC( self, macstr ):
70
        """Set the MAC address for an interface.
71
           macstr: MAC address as string"""
72
        self.mac = macstr
73
        return ( self.ifconfig( 'down' ) +
74
                 self.ifconfig( 'hw', 'ether', macstr ) +
75
                 self.ifconfig( 'up' ) )
76

    
77
    _ipMatchRegex = re.compile( r'\d+\.\d+\.\d+\.\d+' )
78
    _macMatchRegex = re.compile( r'..:..:..:..:..:..' )
79

    
80
    def updateIP( self ):
81
        "Return updated IP address based on ifconfig"
82
        ifconfig = self.ifconfig()
83
        ips = self._ipMatchRegex.findall( ifconfig )
84
        self.ip = ips[ 0 ] if ips else None
85
        return self.ip
86

    
87
    def updateMAC( self ):
88
        "Return updated MAC address based on ifconfig"
89
        ifconfig = self.ifconfig()
90
        macs = self._macMatchRegex.findall( ifconfig )
91
        self.mac = macs[ 0 ] if macs else None
92
        return self.mac
93

    
94
    def IP( self ):
95
        "Return IP address"
96
        return self.ip
97

    
98
    def MAC( self ):
99
        "Return MAC address"
100
        return self.mac
101

    
102
    def isUp( self, setUp=False ):
103
        "Return whether interface is up"
104
        if setUp:
105
            self.ifconfig( 'up' )
106
        return "UP" in self.ifconfig()
107

    
108
    def rename( self, newname ):
109
        "Rename interface"
110
        self.ifconfig( 'down' )
111
        result = self.cmd( 'ip link set', self.name, 'name', newname )
112
        self.name = newname
113
        self.ifconfig( 'up' )
114
        return result
115

    
116
    # The reason why we configure things in this way is so
117
    # That the parameters can be listed and documented in
118
    # the config method.
119
    # Dealing with subclasses and superclasses is slightly
120
    # annoying, but at least the information is there!
121

    
122
    def setParam( self, results, method, **param ):
123
        """Internal method: configure a *single* parameter
124
           results: dict of results to update
125
           method: config method name
126
           param: arg=value (ignore if value=None)
127
           value may also be list or dict"""
128
        name, value = param.items()[ 0 ]
129
        f = getattr( self, method, None )
130
        if not f or value is None:
131
            return
132
        if type( value ) is list:
133
            result = f( *value )
134
        elif type( value ) is dict:
135
            result = f( **value )
136
        else:
137
            result = f( value )
138
        results[ name ] = result
139
        return result
140

    
141
    def config( self, mac=None, ip=None, ifconfig=None,
142
                up=True, **_params ):
143
        """Configure Node according to (optional) parameters:
144
           mac: MAC address
145
           ip: IP address
146
           ifconfig: arbitrary interface configuration
147
           Subclasses should override this method and call
148
           the parent class's config(**params)"""
149
        # If we were overriding this method, we would call
150
        # the superclass config method here as follows:
151
        # r = Parent.config( **params )
152
        r = {}
153
        self.setParam( r, 'setMAC', mac=mac )
154
        self.setParam( r, 'setIP', ip=ip )
155
        self.setParam( r, 'isUp', up=up )
156
        self.setParam( r, 'ifconfig', ifconfig=ifconfig )
157
        self.updateIP()
158
        self.updateMAC()
159
        return r
160

    
161
    def delete( self ):
162
        "Delete interface"
163
        self.cmd( 'ip link del ' + self.name )
164
        if self.node.inNamespace:
165
            # Link may have been dumped into root NS
166
            quietRun( 'ip link del ' + self.name )
167

    
168
    def __repr__( self ):
169
        return '<%s %s>' % ( self.__class__.__name__, self.name )
170

    
171
    def __str__( self ):
172
        return self.name
173

    
174

    
175
class TCIntf( Intf ):
176
    """Interface customized by tc (traffic control) utility
177
       Allows specification of bandwidth limits (various methods)
178
       as well as delay, loss and max queue length"""
179

    
180
    def bwCmds( self, bw=None, speedup=0, use_hfsc=False, use_tbf=False,
181
                latency_ms=None, enable_ecn=False, enable_red=False ):
182
        "Return tc commands to set bandwidth"
183

    
184
        cmds, parent = [], ' root '
185

    
186
        if bw and ( bw < 0 or bw > 1000 ):
187
            error( 'Bandwidth', bw, 'is outside range 0..1000 Mbps\n' )
188

    
189
        elif bw is not None:
190
            # BL: this seems a bit brittle...
191
            if ( speedup > 0 and
192
                 self.node.name[0:1] == 's' ):
193
                bw = speedup
194
            # This may not be correct - we should look more closely
195
            # at the semantics of burst (and cburst) to make sure we
196
            # are specifying the correct sizes. For now I have used
197
            # the same settings we had in the mininet-hifi code.
198
            if use_hfsc:
199
                cmds += [ '%s qdisc add dev %s root handle 5:0 hfsc default 1',
200
                          '%s class add dev %s parent 5:0 classid 5:1 hfsc sc '
201
                          + 'rate %fMbit ul rate %fMbit' % ( bw, bw ) ]
202
            elif use_tbf:
203
                if latency_ms is None:
204
                    latency_ms = 15 * 8 / bw
205
                cmds += [ '%s qdisc add dev %s root handle 5: tbf ' +
206
                          'rate %fMbit burst 15000 latency %fms' %
207
                          ( bw, latency_ms ) ]
208
            else:
209
                cmds += [ '%s qdisc add dev %s root handle 5:0 htb default 1',
210
                          '%s class add dev %s parent 5:0 classid 5:1 htb ' +
211
                          'rate %fMbit burst 15k' % bw ]
212
            parent = ' parent 5:1 '
213

    
214
            # ECN or RED
215
            if enable_ecn:
216
                cmds += [ '%s qdisc add dev %s' + parent +
217
                          'handle 6: red limit 1000000 ' +
218
                          'min 30000 max 35000 avpkt 1500 ' +
219
                          'burst 20 ' +
220
                          'bandwidth %fmbit probability 1 ecn' % bw ]
221
                parent = ' parent 6: '
222
            elif enable_red:
223
                cmds += [ '%s qdisc add dev %s' + parent +
224
                          'handle 6: red limit 1000000 ' +
225
                          'min 30000 max 35000 avpkt 1500 ' +
226
                          'burst 20 ' +
227
                          'bandwidth %fmbit probability 1' % bw ]
228
                parent = ' parent 6: '
229
        return cmds, parent
230

    
231
    @staticmethod
232
    def delayCmds( parent, delay=None, jitter=None,
233
                   loss=None, max_queue_size=None ):
234
        "Internal method: return tc commands for delay and loss"
235
        cmds = []
236
        if delay and delay < 0:
237
            error( 'Negative delay', delay, '\n' )
238
        elif jitter and jitter < 0:
239
            error( 'Negative jitter', jitter, '\n' )
240
        elif loss and ( loss < 0 or loss > 100 ):
241
            error( 'Bad loss percentage', loss, '%%\n' )
242
        else:
243
            # Delay/jitter/loss/max queue size
244
            netemargs = '%s%s%s%s' % (
245
                'delay %s ' % delay if delay is not None else '',
246
                '%s ' % jitter if jitter is not None else '',
247
                'loss %d ' % loss if loss is not None else '',
248
                'limit %d' % max_queue_size if max_queue_size is not None
249
                else '' )
250
            if netemargs:
251
                cmds = [ '%s qdisc add dev %s ' + parent +
252
                         ' handle 10: netem ' +
253
                         netemargs ]
254
                parent = ' parent 10:1 '
255
        return cmds, parent
256

    
257
    def tc( self, cmd, tc='tc' ):
258
        "Execute tc command for our interface"
259
        c = cmd % (tc, self)  # Add in tc command and our name
260
        debug(" *** executing command: %s\n" % c)
261
        return self.cmd( c )
262

    
263
    def config( self, bw=None, delay=None, jitter=None, loss=None,
264
                disable_gro=True, speedup=0, use_hfsc=False, use_tbf=False,
265
                latency_ms=None, enable_ecn=False, enable_red=False,
266
                max_queue_size=None, **params ):
267
        "Configure the port and set its properties."
268

    
269
        result = Intf.config( self, **params)
270

    
271
        # Disable GRO
272
        if disable_gro:
273
            self.cmd( 'ethtool -K %s gro off' % self )
274

    
275
        # Optimization: return if nothing else to configure
276
        # Question: what happens if we want to reset things?
277
        if ( bw is None and not delay and not loss
278
             and max_queue_size is None ):
279
            return
280

    
281
        # Clear existing configuration
282
        cmds = [ '%s qdisc del dev %s root' ]
283

    
284
        # Bandwidth limits via various methods
285
        bwcmds, parent = self.bwCmds( bw=bw, speedup=speedup,
286
                                      use_hfsc=use_hfsc, use_tbf=use_tbf,
287
                                      latency_ms=latency_ms,
288
                                      enable_ecn=enable_ecn,
289
                                      enable_red=enable_red )
290
        cmds += bwcmds
291

    
292
        # Delay/jitter/loss/max_queue_size using netem
293
        delaycmds, parent = self.delayCmds( delay=delay, jitter=jitter,
294
                                loss=loss, max_queue_size=max_queue_size,
295
                                parent=parent )
296
        cmds += delaycmds
297

    
298
        # Ugly but functional: display configuration info
299
        stuff = ( ( [ '%.2fMbit' % bw ] if bw is not None else [] ) +
300
                  ( [ '%s delay' % delay ] if delay is not None else [] ) +
301
                  ( [ '%s jitter' % jitter ] if jitter is not None else [] ) +
302
                  ( ['%d%% loss' % loss ] if loss is not None else [] ) +
303
                  ( [ 'ECN' ] if enable_ecn else [ 'RED' ]
304
                    if enable_red else [] ) )
305
        info( '(' + ' '.join( stuff ) + ') ' )
306

    
307
        # Execute all the commands in our node
308
        debug("at map stage w/cmds: %s\n" % cmds)
309
        tcoutputs = [ self.tc(cmd) for cmd in cmds ]
310
        debug( "cmds:", cmds, '\n' )
311
        debug( "outputs:", tcoutputs, '\n' )
312
        result[ 'tcoutputs'] = tcoutputs
313
        result[ 'parent' ] = parent
314

    
315
        return result
316

    
317

    
318
class Link( object ):
319

    
320
    """A basic link is just a veth pair.
321
       Other types of links could be tunnels, link emulators, etc.."""
322

    
323
    def __init__( self, node1, node2, port1=None, port2=None,
324
                  intfName1=None, intfName2=None,
325
                  intf=Intf, cls1=None, cls2=None, params1=None,
326
                  params2=None ):
327
        """Create veth link to another node, making two new interfaces.
328
           node1: first node
329
           node2: second node
330
           port1: node1 port number (optional)
331
           port2: node2 port number (optional)
332
           intf: default interface class/constructor
333
           cls1, cls2: optional interface-specific constructors
334
           intfName1: node1 interface name (optional)
335
           intfName2: node2  interface name (optional)
336
           params1: parameters for interface 1
337
           params2: parameters for interface 2"""
338
        # This is a bit awkward; it seems that having everything in
339
        # params would be more orthogonal, but being able to specify
340
        # in-line arguments is more convenient!
341
        if port1 is None:
342
            port1 = node1.newPort()
343
        if port2 is None:
344
            port2 = node2.newPort()
345
        if not intfName1:
346
            intfName1 = self.intfName( node1, port1 )
347
        if not intfName2:
348
            intfName2 = self.intfName( node2, port2 )
349

    
350
        self.makeIntfPair( intfName1, intfName2 )
351

    
352
        if not cls1:
353
            cls1 = intf
354
        if not cls2:
355
            cls2 = intf
356
        if not params1:
357
            params1 = {}
358
        if not params2:
359
            params2 = {}
360

    
361
        intf1 = cls1( name=intfName1, node=node1, port=port1,
362
                      link=self, **params1  )
363
        intf2 = cls2( name=intfName2, node=node2, port=port2,
364
                      link=self, **params2 )
365

    
366
        # All we are is dust in the wind, and our two interfaces
367
        self.intf1, self.intf2 = intf1, intf2
368

    
369
    @classmethod
370
    def intfName( cls, node, n ):
371
        "Construct a canonical interface name node-ethN for interface n."
372
        return node.name + '-eth' + repr( n )
373

    
374
    @classmethod
375
    def makeIntfPair( cls, intf1, intf2 ):
376
        """Create pair of interfaces
377
           intf1: name of interface 1
378
           intf2: name of interface 2
379
           (override this class method [and possibly delete()]
380
           to change link type)"""
381
        makeIntfPair( intf1, intf2  )
382

    
383
    def delete( self ):
384
        "Delete this link"
385
        self.intf1.delete()
386
        self.intf2.delete()
387

    
388
    def __str__( self ):
389
        return '%s<->%s' % ( self.intf1, self.intf2 )
390

    
391
class TCLink( Link ):
392
    "Link with symmetric TC interfaces configured via opts"
393
    def __init__( self, node1, node2, port1=None, port2=None,
394
                  intfName1=None, intfName2=None, **params ):
395
        Link.__init__( self, node1, node2, port1=port1, port2=port2,
396
                       intfName1=intfName1, intfName2=intfName2,
397
                       cls1=TCIntf,
398
                       cls2=TCIntf,
399
                       params1=params,
400
                       params2=params)