Statistics
| Branch: | Tag: | Revision:

mininet / examples / cluster.py @ 18aab5b7

History | View | Annotate | Download (30.9 KB)

1
#!/usr/bin/python
2

    
3
"""
4
cluster.py: prototyping/experimentation for distributed Mininet,
5
            aka Mininet: Cluster Edition
6

7
Author: Bob Lantz
8

9
Core classes:
10

11
    RemoteNode: a Node() running on a remote server
12
    RemoteOVSSwitch(): an OVSSwitch() running on a remote server
13
    RemoteLink: a Link() on a remote server
14
    Tunnel: a Link() between a local Node() and a RemoteNode()
15

16
These are largely interoperable with local objects.
17

18
- One Mininet to rule them all
19

20
It is important that the same topologies, APIs, and CLI can be used
21
with minimal or no modification in both local and distributed environments.
22

23
- Multiple placement models
24

25
Placement should be as easy as possible. We should provide basic placement
26
support and also allow for explicit placement.
27

28
Questions:
29

30
What is the basic communication mechanism?
31

32
To start with? Probably a single multiplexed ssh connection between each
33
pair of mininet servers that needs to communicate.
34

35
How are tunnels created?
36

37
We have several options including ssh, GRE, OF capsulator, socat, VDE, l2tp,
38
etc..  It's not clear what the best one is.  For now, we use ssh tunnels since
39
they are encrypted and semi-automatically shared.  We will probably want to
40
support GRE as well because it's very easy to set up with OVS.
41

42
How are tunnels destroyed?
43

44
They are destroyed when the links are deleted in Mininet.stop()
45

46
How does RemoteNode.popen() work?
47

48
It opens a shared ssh connection to the remote server and attaches to
49
the namespace using mnexec -a -g.
50

51
Is there any value to using Paramiko vs. raw ssh?
52

53
Maybe, but it doesn't seem to support L2 tunneling.
54

55
Should we preflight the entire network, including all server-to-server
56
connections?
57

58
Yes! We don't yet do this with remote server-to-server connections yet.
59

60
Should we multiplex the link ssh connections?
61

62
Yes, this is done automatically with ControlMaster=auto.
63

64
Note on ssh and DNS:
65
Please add UseDNS: no to your /etc/ssh/sshd_config!!!
66

67
Things to do:
68

69
- asynchronous/pipelined/parallel startup
70
- ssh debugging/profiling
71
- make connections into real objects
72
- support for other tunneling schemes
73
- tests and benchmarks
74
- hifi support (e.g. delay compensation)
75
"""
76

    
77
from mininet.node import Node, Host, OVSSwitch, Controller
78
from mininet.link import Link, Intf
79
from mininet.net import Mininet
80
from mininet.topo import LinearTopo
81
from mininet.topolib import TreeTopo
82
from mininet.util import quietRun, makeIntfPair, errRun, retry
83
from mininet.examples.clustercli import CLI
84
from mininet.log import setLogLevel, debug, info, error
85

    
86
from signal import signal, SIGINT, SIG_IGN
87
from subprocess import Popen, PIPE, STDOUT
88
import os
89
from random import randrange
90
import sys
91
import re
92

    
93
from distutils.version import StrictVersion
94

    
95
# BL note: so little code is required for remote nodes,
96
# we will probably just want to update the main Node()
97
# class to enable it for remote access! However, there
98
# are a large number of potential failure conditions with
99
# remote nodes which we may want to detect and handle.
100
# Another interesting point is that we could put everything
101
# in a mix-in class and easily add cluster mode to 2.0.
102

    
103
class RemoteMixin( object ):
104

    
105
    "A mix-in class to turn local nodes into remote nodes"
106

    
107
    # ssh base command
108
    # -q: don't print stupid diagnostic messages
109
    # BatchMode yes: don't ask for password
110
    # ForwardAgent yes: forward authentication credentials
111
    sshbase = [ 'ssh', '-q',
112
                '-o', 'BatchMode=yes',
113
                '-o', 'ForwardAgent=yes', '-tt' ]
114

    
115
    def __init__( self, name, server='localhost', user=None, serverIP=None,
116
                  controlPath=False, splitInit=False, **kwargs):
117
        """Instantiate a remote node
118
           name: name of remote node
119
           server: remote server (optional)
120
           user: user on remote server (optional)
121
           controlPath: specify shared ssh control path (optional)
122
           splitInit: split initialization?
123
           **kwargs: see Node()"""
124
        # We connect to servers by IP address
125
        self.server = server if server else 'localhost'
126
        self.serverIP = ( serverIP if serverIP
127
                          else self.findServerIP( self.server ) )
128
        self.user = user if user else self.findUser()
129
        if controlPath is True:
130
            # Set a default control path for shared SSH connections
131
            controlPath = '/tmp/mn-%r@%h:%p'
132
        self.controlPath = controlPath
133
        self.splitInit = splitInit
134
        if self.user and self.server != 'localhost':
135
            self.dest = '%s@%s' % ( self.user, self.serverIP )
136
            self.sshcmd = [ 'sudo', '-E', '-u', self.user ] + self.sshbase
137
            if self.controlPath:
138
                self.sshcmd += [ '-o', 'ControlPath=' + self.controlPath,
139
                                 '-o', 'ControlMaster=auto',
140
                                 '-o', 'ControlPersist=' + '1' ]
141
            self.sshcmd = self.sshcmd + [ self.dest ]
142
            self.isRemote = True
143
        else:
144
            self.dest = None
145
            self.sshcmd = []
146
            self.isRemote = False
147
        # Satisfy pylint
148
        self.shell, self.pid, self.cmd = None, None, None
149
        super( RemoteMixin, self ).__init__( name, **kwargs )
150

    
151
    @staticmethod
152
    def findUser():
153
        "Try to return logged-in (usually non-root) user"
154
        return (
155
            # If we're running sudo
156
            os.environ.get( 'SUDO_USER', False ) or
157
            # Logged-in user (if we have a tty)
158
            ( quietRun( 'who am i' ).split() or [ False ] )[ 0 ] or
159
            # Give up and return effective user
160
            quietRun( 'whoami' ) )
161

    
162
    # Determine IP address of local host
163
    _ipMatchRegex = re.compile( r'\d+\.\d+\.\d+\.\d+' )
164

    
165
    @classmethod
166
    def findServerIP( cls, server ):
167
        "Return our server's IP address"
168
        # First, check for an IP address
169
        ipmatch = cls._ipMatchRegex.findall( server )
170
        if ipmatch:
171
            return ipmatch[ 0 ]
172
        # Otherwise, look up remote server
173
        output = quietRun( 'getent ahostsv4 %s' % server )
174
        ips = cls._ipMatchRegex.findall( output )
175
        ip = ips[ 0 ] if ips else None
176
        return ip
177

    
178
    # Command support via shell process in namespace
179
    def startShell( self, *args, **kwargs ):
180
        "Start a shell process for running commands"
181
        if self.isRemote:
182
            kwargs.update( mnopts='-c' )
183
        super( RemoteMixin, self ).startShell( *args, **kwargs )
184
        if self.splitInit:
185
            self.sendCmd( 'echo $$' )
186
        else:
187
            self.pid = int( self.cmd( 'echo $$' ) )
188

    
189
    def finishInit( self ):
190
        "Wait for split initialization to complete"
191
        assert self  # please pylint
192
        self.pid = int( self.waitOutput() )
193

    
194
    def rpopen( self, *cmd, **opts ):
195
        "Return a Popen object on underlying server in root namespace"
196
        params = { 'stdin': PIPE,
197
                   'stdout': PIPE,
198
                   'stderr': STDOUT,
199
                   'sudo': True }
200
        params.update( opts )
201
        return self._popen( *cmd, **params )
202

    
203
    def rcmd( self, *cmd, **opts):
204
        """rcmd: run a command on underlying server
205
           in root namespace
206
           args: string or list of strings
207
           returns: stdout and stderr"""
208
        popen = self.rpopen( *cmd, **opts )
209
        # print 'RCMD: POPEN:', popen
210
        # These loops are tricky to get right.
211
        # Once the process exits, we can read
212
        # EOF twice if necessary.
213
        result = ''
214
        while True:
215
            poll = popen.poll()
216
            result += popen.stdout.read()
217
            if poll is not None:
218
                break
219
        return result
220

    
221
    @staticmethod
222
    def _ignoreSignal():
223
        "Detach from process group to ignore all signals"
224
        os.setpgrp()
225

    
226
    def _popen( self, cmd, sudo=True, tt=True, **params):
227
        """Spawn a process on a remote node
228
            cmd: remote command to run (list)
229
            **params: parameters to Popen()
230
            returns: Popen() object"""
231
        if type( cmd ) is str:
232
            cmd = cmd.split()
233
        if self.isRemote:
234
            if sudo:
235
                cmd = [ 'sudo', '-E' ] + cmd
236
            if tt:
237
                cmd = self.sshcmd + cmd
238
            else:
239
                # Hack: remove -tt
240
                sshcmd = list( self.sshcmd )
241
                sshcmd.remove( '-tt' )
242
                cmd = sshcmd + cmd
243
        else:
244
            if self.user and not sudo:
245
                # Drop privileges
246
                cmd = [ 'sudo', '-E', '-u', self.user ] + cmd
247
        params.update( preexec_fn=self._ignoreSignal )
248
        debug( '_popen', ' '.join(cmd), params )
249
        popen = super( RemoteMixin, self )._popen( cmd, **params )
250
        return popen
251

    
252
    def popen( self, *args, **kwargs ):
253
        "Override: disable -tt"
254
        return super( RemoteMixin, self).popen( *args, tt=False, **kwargs )
255

    
256
    def addIntf( self, *args, **kwargs ):
257
        "Override: use RemoteLink.moveIntf"
258
        return super( RemoteMixin, self).addIntf( *args,
259
                        moveIntfFn=RemoteLink.moveIntf, **kwargs )
260

    
261
    def cleanup( self ):
262
        "Help python collect its garbage."
263
        # Intfs may end up in root NS
264
        for intfName in self.intfNames():
265
            if self.name in intfName:
266
                self.rcmd( 'ip link del ' + intfName )
267
        self.shell = None
268

    
269
class RemoteNode( RemoteMixin, Node ):
270
    "A node on a remote server"
271
    pass
272

    
273

    
274
class RemoteHost( RemoteNode ):
275
    "A RemoteHost is simply a RemoteNode"
276
    pass
277

    
278

    
279
class RemoteOVSSwitch( RemoteMixin, OVSSwitch ):
280
    "Remote instance of Open vSwitch"
281
    OVSVersions = {}
282
    def isOldOVS( self ):
283
        "Is remote switch using an old OVS version?"
284
        cls = type( self )
285
        if self.server not in cls.OVSVersions:
286
            vers = self.cmd( 'ovs-vsctl --version' )
287
            cls.OVSVersions[ self.server ] = re.findall(
288
                r'\d+\.\d+', vers )[ 0 ]
289
        return ( StrictVersion( cls.OVSVersions[ self.server ] ) <
290
                StrictVersion( '1.10' ) )
291

    
292

    
293

    
294
class RemoteLink( Link ):
295

    
296
    "A RemoteLink is a link between nodes which may be on different servers"
297

    
298
    def __init__( self, node1, node2, **kwargs ):
299
        """Initialize a RemoteLink
300
           see Link() for parameters"""
301
        # Create links on remote node
302
        self.node1 = node1
303
        self.node2 = node2
304
        self.tunnel = None
305
        kwargs.setdefault( 'params1', {} )
306
        kwargs.setdefault( 'params2', {} )
307
        self.cmd = None  # satisfy pylint
308
        Link.__init__( self, node1, node2, **kwargs )
309

    
310
    def stop( self ):
311
        "Stop this link"
312
        if self.tunnel:
313
            self.tunnel.terminate()
314
        self.tunnel = None
315

    
316
    def makeIntfPair( self, intfname1, intfname2, addr1=None, addr2=None ):
317
        """Create pair of interfaces
318
            intfname1: name of interface 1
319
            intfname2: name of interface 2
320
            (override this method [and possibly delete()]
321
            to change link type)"""
322
        node1, node2 = self.node1, self.node2
323
        server1 = getattr( node1, 'server', 'localhost' )
324
        server2 = getattr( node2, 'server', 'localhost' )
325
        if server1 == 'localhost' and server2 == 'localhost':
326
            # Local link
327
            return makeIntfPair( intfname1, intfname2, addr1, addr2 )
328
        elif server1 == server2:
329
            # Remote link on same remote server
330
            return makeIntfPair( intfname1, intfname2, addr1, addr2,
331
                                 runCmd=node1.rcmd )
332
        # Otherwise, make a tunnel
333
        self.tunnel = self.makeTunnel( node1, node2, intfname1, intfname2,
334
                                       addr1, addr2 )
335
        return self.tunnel
336

    
337
    @staticmethod
338
    def moveIntf( intf, node, printError=True ):
339
        """Move remote interface from root ns to node
340
            intf: string, interface
341
            dstNode: destination Node
342
            srcNode: source Node or None (default) for root ns
343
            printError: if true, print error"""
344
        intf = str( intf )
345
        cmd = 'ip link set %s netns %s' % ( intf, node.pid )
346
        node.rcmd( cmd )
347
        links = node.cmd( 'ip link show' )
348
        if not ' %s:' % intf in links:
349
            if printError:
350
                error( '*** Error: RemoteLink.moveIntf: ' + intf +
351
                      ' not successfully moved to ' + node.name + '\n' )
352
            return False
353
        return True
354

    
355
    def makeTunnel( self, node1, node2, intfname1, intfname2,
356
                    addr1=None, addr2=None ):
357
        "Make a tunnel across switches on different servers"
358
        # We should never try to create a tunnel to ourselves!
359
        assert node1.server != 'localhost' or node2.server != 'localhost'
360
        # And we can't ssh into this server remotely as 'localhost',
361
        # so try again swappping node1 and node2
362
        if node2.server == 'localhost':
363
            return self.makeTunnel( node2, node1, intfname2, intfname1,
364
                                    addr2, addr1 )
365
        # 1. Create tap interfaces
366
        for node in node1, node2:
367
            # For now we are hard-wiring tap9, which we will rename
368
            node.rcmd( 'ip link delete tap9', stderr=PIPE )
369
            cmd = 'ip tuntap add dev tap9 mode tap user ' + node.user
370
            node.rcmd( cmd )
371
            links = node.rcmd( 'ip link show' )
372
            # print 'after add, links =', links
373
            assert 'tap9' in links
374
        # 2. Create ssh tunnel between tap interfaces
375
        # -n: close stdin
376
        dest = '%s@%s' % ( node2.user, node2.serverIP )
377
        cmd = [ 'ssh', '-n', '-o', 'Tunnel=Ethernet', '-w', '9:9',
378
                dest, 'echo @' ]
379
        self.cmd = cmd
380
        tunnel = node1.rpopen( cmd, sudo=False )
381
        # When we receive the character '@', it means that our
382
        # tunnel should be set up
383
        debug( 'Waiting for tunnel to come up...\n' )
384
        ch = tunnel.stdout.read( 1 )
385
        if ch != '@':
386
            error( 'makeTunnel:\n',
387
                   'Tunnel setup failed for',
388
                   '%s:%s' % ( node1, node1.dest ), 'to',
389
                   '%s:%s\n' % ( node2, node2.dest ),
390
                  'command was:', cmd, '\n' )
391
            tunnel.terminate()
392
            tunnel.wait()
393
            error( ch + tunnel.stdout.read() )
394
            error( tunnel.stderr.read() )
395
            sys.exit( 1 )
396
        # 3. Move interfaces if necessary
397
        for node in node1, node2:
398
            if node.inNamespace:
399
                retry( 3, .01, RemoteLink.moveIntf, 'tap9', node )
400
        # 4. Rename tap interfaces to desired names
401
        for node, intf, addr in ( ( node1, intfname1, addr1 ),
402
                            ( node2, intfname2, addr2 ) ):
403
            if not addr:
404
                node.cmd( 'ip link set tap9 name', intf )
405
            else:
406
                node.cmd( 'ip link set tap9 name', intf, 'address', addr )
407
        for node, intf in ( ( node1, intfname1 ), ( node2, intfname2 ) ):
408
            assert intf in node.cmd( 'ip link show' )
409
        return tunnel
410

    
411
    def status( self ):
412
        "Detailed representation of link"
413
        if self.tunnel:
414
            if self.tunnel.poll() is not None:
415
                status = "Tunnel EXITED %s" % self.tunnel.returncode
416
            else:
417
                status = "Tunnel Running (%s: %s)" % (
418
                    self.tunnel.pid, self.cmd )
419
        else:
420
            status = "OK"
421
        result = "%s %s" % ( Link.status( self ), status )
422
        return result
423

    
424

    
425
# Some simple placement algorithms for MininetCluster
426

    
427
class Placer( object ):
428
    "Node placement algorithm for MininetCluster"
429

    
430
    def __init__( self, servers=None, nodes=None, hosts=None,
431
                 switches=None, controllers=None, links=None ):
432
        """Initialize placement object
433
           servers: list of servers
434
           nodes: list of all nodes
435
           hosts: list of hosts
436
           switches: list of switches
437
           controllers: list of controllers
438
           links: list of links
439
           (all arguments are optional)
440
           returns: server"""
441
        self.servers = servers or []
442
        self.nodes = nodes or []
443
        self.hosts = hosts or []
444
        self.switches = switches or []
445
        self.controllers = controllers or []
446
        self.links = links or []
447

    
448
    def place( self, node ):
449
        "Return server for a given node"
450
        assert self, node  # satisfy pylint
451
        # Default placement: run locally
452
        return 'localhost'
453

    
454

    
455
class RandomPlacer( Placer ):
456
    "Random placement"
457
    def place( self, nodename ):
458
        """Random placement function
459
            nodename: node name"""
460
        assert nodename  # please pylint
461
        # This may be slow with lots of servers
462
        return self.servers[ randrange( 0, len( self.servers ) ) ]
463

    
464

    
465
class RoundRobinPlacer( Placer ):
466
    """Round-robin placement
467
       Note this will usually result in cross-server links between
468
       hosts and switches"""
469

    
470
    def __init__( self, *args, **kwargs ):
471
        Placer.__init__( self, *args, **kwargs )
472
        self.next = 0
473

    
474
    def place( self, nodename ):
475
        """Round-robin placement function
476
            nodename: node name"""
477
        assert nodename  # please pylint
478
        # This may be slow with lots of servers
479
        server = self.servers[ self.next ]
480
        self.next = ( self.next + 1 ) % len( self.servers )
481
        return server
482

    
483

    
484
class SwitchBinPlacer( Placer ):
485
    """Place switches (and controllers) into evenly-sized bins,
486
       and attempt to co-locate hosts and switches"""
487

    
488
    def __init__( self, *args, **kwargs ):
489
        Placer.__init__( self, *args, **kwargs )
490
        # Easy lookup for servers and node sets
491
        self.servdict = dict( enumerate( self.servers ) )
492
        self.hset = frozenset( self.hosts )
493
        self.sset = frozenset( self.switches )
494
        self.cset = frozenset( self.controllers )
495
        # Server and switch placement indices
496
        self.placement =  self.calculatePlacement()
497

    
498
    @staticmethod
499
    def bin( nodes, servers ):
500
        "Distribute nodes evenly over servers"
501
        # Calculate base bin size
502
        nlen = len( nodes )
503
        slen = len( servers )
504
        # Basic bin size
505
        quotient = int( nlen / slen )
506
        binsizes = { server: quotient for server in servers }
507
        # Distribute remainder
508
        remainder = nlen % slen
509
        for server in servers[ 0 : remainder ]:
510
            binsizes[ server ] += 1
511
        # Create binsize[ server ] tickets for each server
512
        tickets = sum( [ binsizes[ server ] * [ server ]
513
                         for server in servers ], [] )
514
        # And assign one ticket to each node
515
        return { node: ticket for node, ticket in zip( nodes, tickets ) }
516

    
517
    def calculatePlacement( self ):
518
        "Pre-calculate node placement"
519
        placement = {}
520
        # Create host-switch connectivity map,
521
        # associating host with last switch that it's
522
        # connected to
523
        switchFor = {}
524
        for src, dst in self.links:
525
            if src in self.hset and dst in self.sset:
526
                switchFor[ src ] = dst
527
            if dst in self.hset and src in self.sset:
528
                switchFor[ dst ] = src
529
        # Place switches
530
        placement = self.bin( self.switches, self.servers )
531
        # Place controllers and merge into placement dict
532
        placement.update( self.bin( self.controllers, self.servers ) )
533
        # Co-locate hosts with their switches
534
        for h in self.hosts:
535
            if h in placement:
536
                # Host is already placed - leave it there
537
                continue
538
            if h in switchFor:
539
                placement[ h ] = placement[ switchFor[ h ] ]
540
            else:
541
                raise Exception(
542
                        "SwitchBinPlacer: cannot place isolated host " + h )
543
        return placement
544

    
545
    def place( self, node ):
546
        """Simple placement algorithm:
547
           place switches into evenly sized bins,
548
           and place hosts near their switches"""
549
        return self.placement[ node ]
550

    
551

    
552
class HostSwitchBinPlacer( Placer ):
553
    """Place switches *and hosts* into evenly-sized bins
554
       Note that this will usually result in cross-server
555
       links between hosts and switches"""
556

    
557
    def __init__( self, *args, **kwargs ):
558
        Placer.__init__( self, *args, **kwargs )
559
        # Calculate bin sizes
560
        scount = len( self.servers )
561
        self.hbin = max( int( len( self.hosts ) / scount ), 1 )
562
        self.sbin = max( int( len( self.switches ) / scount ), 1 )
563
        self.cbin = max( int( len( self.controllers ) / scount ) , 1 )
564
        info( 'scount:', scount )
565
        info( 'bins:', self.hbin, self.sbin, self.cbin, '\n' )
566
        self.servdict = dict( enumerate( self.servers ) )
567
        self.hset = frozenset( self.hosts )
568
        self.sset = frozenset( self.switches )
569
        self.cset = frozenset( self.controllers )
570
        self.hind, self.sind, self.cind = 0, 0, 0
571

    
572
    def place( self, nodename ):
573
        """Simple placement algorithm:
574
            place nodes into evenly sized bins"""
575
        # Place nodes into bins
576
        if nodename in self.hset:
577
            server = self.servdict[ self.hind / self.hbin ]
578
            self.hind += 1
579
        elif nodename in self.sset:
580
            server = self.servdict[ self.sind / self.sbin ]
581
            self.sind += 1
582
        elif nodename in self.cset:
583
            server = self.servdict[ self.cind / self.cbin ]
584
            self.cind += 1
585
        else:
586
            info( 'warning: unknown node', nodename )
587
            server = self.servdict[ 0 ]
588
        return server
589

    
590

    
591

    
592
# The MininetCluster class is not strictly necessary.
593
# However, it has several purposes:
594
# 1. To set up ssh connection sharing/multiplexing
595
# 2. To pre-flight the system so that everything is more likely to work
596
# 3. To allow connection/connectivity monitoring
597
# 4. To support pluggable placement algorithms
598

    
599
class MininetCluster( Mininet ):
600

    
601
    "Cluster-enhanced version of Mininet class"
602

    
603
    # Default ssh command
604
    # BatchMode yes: don't ask for password
605
    # ForwardAgent yes: forward authentication credentials
606
    sshcmd = [ 'ssh', '-o', 'BatchMode=yes', '-o', 'ForwardAgent=yes' ]
607

    
608
    def __init__( self, *args, **kwargs ):
609
        """servers: a list of servers to use (note: include
610
           localhost or None to use local system as well)
611
           user: user name for server ssh
612
           placement: Placer() subclass"""
613
        params = { 'host': RemoteHost,
614
                   'switch': RemoteOVSSwitch,
615
                   'link': RemoteLink,
616
                   'precheck': True }
617
        params.update( kwargs )
618
        servers = params.pop( 'servers', [ 'localhost' ] )
619
        servers = [ s if s else 'localhost' for s in servers ]
620
        self.servers = servers
621
        self.serverIP = params.pop( 'serverIP', {} )
622
        if not self.serverIP:
623
            self.serverIP = { server: RemoteMixin.findServerIP( server )
624
                              for server in self.servers }
625
        self.user = params.pop( 'user', RemoteMixin.findUser() )
626
        if params.pop( 'precheck' ):
627
            self.precheck()
628
        self.connections = {}
629
        self.placement = params.pop( 'placement', SwitchBinPlacer )
630
        # Make sure control directory exists
631
        self.cdir = os.environ[ 'HOME' ] + '/.ssh/mn'
632
        errRun( [ 'mkdir', '-p', self.cdir ] )
633
        Mininet.__init__( self, *args, **params )
634

    
635
    def popen( self, cmd ):
636
        "Popen() for server connections"
637
        assert self  # please pylint
638
        old = signal( SIGINT, SIG_IGN )
639
        conn = Popen( cmd, stdin=PIPE, stdout=PIPE, close_fds=True )
640
        signal( SIGINT, old )
641
        return conn
642

    
643
    def baddLink( self, *args, **kwargs ):
644
        "break addlink for testing"
645
        pass
646

    
647
    def precheck( self ):
648
        """Pre-check to make sure connection works and that
649
           we can call sudo without a password"""
650
        result = 0
651
        info( '*** Checking servers\n' )
652
        for server in self.servers:
653
            ip = self.serverIP[ server ]
654
            if not server or server == 'localhost':
655
                continue
656
            info( server, '' )
657
            dest = '%s@%s' % ( self.user, ip )
658
            cmd = [ 'sudo', '-E', '-u', self.user ]
659
            cmd += self.sshcmd + [ '-n', dest, 'sudo true' ]
660
            debug( ' '.join( cmd ), '\n' )
661
            _out, _err, code = errRun( cmd )
662
            if code != 0:
663
                error( '\nstartConnection: server connection check failed '
664
                       'to %s using command:\n%s\n'
665
                        % ( server, ' '.join( cmd ) ) )
666
            result |= code
667
        if result:
668
            error( '*** Server precheck failed.\n'
669
                   '*** Make sure that the above ssh command works correctly.\n'
670
                   '*** You may also need to run mn -c on all nodes, and/or\n'
671
                   '*** use sudo -E.\n' )
672
            sys.exit( 1 )
673
        info( '\n' )
674

    
675
    def modifiedaddHost( self, *args, **kwargs ):
676
        "Slightly modify addHost"
677
        assert self  # please pylint
678
        kwargs[ 'splitInit' ] = True
679
        return Mininet.addHost( *args, **kwargs )
680

    
681

    
682
    def placeNodes( self ):
683
        """Place nodes on servers (if they don't have a server), and
684
           start shell processes"""
685
        if not self.servers or not self.topo:
686
            # No shirt, no shoes, no service
687
            return
688
        nodes = self.topo.nodes()
689
        placer = self.placement( servers=self.servers,
690
                                 nodes=self.topo.nodes(),
691
                                 hosts=self.topo.hosts(),
692
                                 switches=self.topo.switches(),
693
                                 links=self.topo.links() )
694
        for node in nodes:
695
            config = self.topo.nodeInfo( node )
696
            # keep local server name consistent accross nodes
697
            if 'server' in config.keys() and config[ 'server' ] == None:
698
                config[ 'server' ] = 'localhost'
699
            server = config.setdefault( 'server', placer.place( node ) )
700
            if server:
701
                config.setdefault( 'serverIP', self.serverIP[ server ] )
702
            info( '%s:%s ' % ( node, server ) )
703
            key = ( None, server )
704
            _dest, cfile, _conn = self.connections.get(
705
                        key, ( None, None, None ) )
706
            if cfile:
707
                config.setdefault( 'controlPath', cfile )
708

    
709
    def addController( self, *args, **kwargs ):
710
        "Patch to update IP address to global IP address"
711
        controller = Mininet.addController( self, *args, **kwargs )
712
        # Update IP address for controller that may not be local
713
        if ( isinstance( controller, Controller)
714
             and controller.IP() == '127.0.0.1'
715
             and ' eth0:' in controller.cmd( 'ip link show' ) ):
716
            Intf( 'eth0', node=controller ).updateIP()
717
        return controller
718

    
719
    def buildFromTopo( self, *args, **kwargs ):
720
        "Start network"
721
        info( '*** Placing nodes\n' )
722
        self.placeNodes()
723
        info( '\n' )
724
        Mininet.buildFromTopo( self, *args, **kwargs )
725

    
726

    
727
def testNsTunnels():
728
    "Test tunnels between nodes in namespaces"
729
    net = Mininet( host=RemoteHost, link=RemoteLink )
730
    h1 = net.addHost( 'h1' )
731
    h2 = net.addHost( 'h2', server='ubuntu2' )
732
    net.addLink( h1, h2 )
733
    net.start()
734
    net.pingAll()
735
    net.stop()
736

    
737
# Manual topology creation with net.add*()
738
#
739
# This shows how node options may be used to manage
740
# cluster placement using the net.add*() API
741

    
742
def testRemoteNet( remote='ubuntu2' ):
743
    "Test remote Node classes"
744
    print '*** Remote Node Test'
745
    net = Mininet( host=RemoteHost, switch=RemoteOVSSwitch,
746
                   link=RemoteLink )
747
    c0 = net.addController( 'c0' )
748
    # Make sure controller knows its non-loopback address
749
    Intf( 'eth0', node=c0 ).updateIP()
750
    print "*** Creating local h1"
751
    h1 = net.addHost( 'h1' )
752
    print "*** Creating remote h2"
753
    h2 = net.addHost( 'h2', server=remote )
754
    print "*** Creating local s1"
755
    s1 = net.addSwitch( 's1' )
756
    print "*** Creating remote s2"
757
    s2 = net.addSwitch( 's2', server=remote )
758
    print "*** Adding links"
759
    net.addLink( h1, s1 )
760
    net.addLink( s1, s2 )
761
    net.addLink( h2, s2 )
762
    net.start()
763
    print 'Mininet is running on', quietRun( 'hostname' ).strip()
764
    for node in c0, h1, h2, s1, s2:
765
        print 'Node', node, 'is running on', node.cmd( 'hostname' ).strip()
766
    net.pingAll()
767
    CLI( net )
768
    net.stop()
769

    
770

    
771
# High-level/Topo API example
772
#
773
# This shows how existing Mininet topologies may be used in cluster
774
# mode by creating node placement functions and a controller which
775
# can be accessed remotely. This implements a very compatible version
776
# of cluster edition with a minimum of code!
777

    
778
remoteHosts = [ 'h2' ]
779
remoteSwitches = [ 's2' ]
780
remoteServer = 'ubuntu2'
781

    
782
def HostPlacer( name, *args, **params ):
783
    "Custom Host() constructor which places hosts on servers"
784
    if name in remoteHosts:
785
        return RemoteHost( name, *args, server=remoteServer, **params )
786
    else:
787
        return Host( name, *args, **params )
788

    
789
def SwitchPlacer( name, *args, **params ):
790
    "Custom Switch() constructor which places switches on servers"
791
    if name in remoteSwitches:
792
        return RemoteOVSSwitch( name, *args, server=remoteServer, **params )
793
    else:
794
        return RemoteOVSSwitch( name, *args, **params )
795

    
796
def ClusterController( *args, **kwargs):
797
    "Custom Controller() constructor which updates its eth0 IP address"
798
    controller = Controller( *args, **kwargs )
799
    # Find out its IP address so that cluster switches can connect
800
    Intf( 'eth0', node=controller ).updateIP()
801
    return controller
802

    
803
def testRemoteTopo():
804
    "Test remote Node classes using Mininet()/Topo() API"
805
    topo = LinearTopo( 2 )
806
    net = Mininet( topo=topo, host=HostPlacer, switch=SwitchPlacer,
807
                  link=RemoteLink, controller=ClusterController )
808
    net.start()
809
    net.pingAll()
810
    net.stop()
811

    
812
# Need to test backwards placement, where each host is on
813
# a server other than its switch!! But seriously we could just
814
# do random switch placement rather than completely random
815
# host placement.
816

    
817
def testRemoteSwitches():
818
    "Test with local hosts and remote switches"
819
    servers = [ 'localhost', 'ubuntu2']
820
    topo = TreeTopo( depth=4, fanout=2 )
821
    net = MininetCluster( topo=topo, servers=servers,
822
                          placement=RoundRobinPlacer )
823
    net.start()
824
    net.pingAll()
825
    net.stop()
826

    
827

    
828
#
829
# For testing and demo purposes it would be nice to draw the
830
# network graph and color it based on server.
831

    
832
# The MininetCluster() class integrates pluggable placement
833
# functions, for maximum ease of use. MininetCluster() also
834
# pre-flights and multiplexes server connections.
835

    
836
def testMininetCluster():
837
    "Test MininetCluster()"
838
    servers = [ 'localhost', 'ubuntu2' ]
839
    topo = TreeTopo( depth=3, fanout=3 )
840
    net = MininetCluster( topo=topo, servers=servers,
841
                          placement=SwitchBinPlacer )
842
    net.start()
843
    net.pingAll()
844
    net.stop()
845

    
846
def signalTest():
847
    "Make sure hosts are robust to signals"
848
    h = RemoteHost( 'h0', server='ubuntu1' )
849
    h.shell.send_signal( SIGINT )
850
    h.shell.poll()
851
    if h.shell.returncode is None:
852
        print 'OK: ', h, 'has not exited'
853
    else:
854
        print 'FAILURE:', h, 'exited with code', h.shell.returncode
855
    h.stop()
856

    
857
if __name__ == '__main__':
858
    setLogLevel( 'info' )
859
    # testRemoteTopo()
860
    # testRemoteNet()
861
    # testMininetCluster()
862
    # testRemoteSwitches()
863
    signalTest()