Statistics
| Branch: | Tag: | Revision:

mininet / examples / cluster.py @ 80d647a9

History | View | Annotate | Download (29.6 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, SIGHUP, SIG_IGN
87
from subprocess import Popen, PIPE, STDOUT
88
import os
89
from random import randrange
90
from sys import exit
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=None, user=None, serverIP=None,
116
                  controlPath='/tmp/mn-%r@%h:%p', 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: ssh control path template (optional)
122
           splitInit: split initialization?
123
           **kwargs: see Node()"""
124
        # We connect to servers by IP address
125
        if server == 'localhost':
126
            server = None
127
        self.server = server
128
        self.serverIP = serverIP if serverIP else self.findServerIP( server )
129
        self.user = user if user else self.findUser()
130
        if self.user and self.server:
131
            self.dest = '%s@%s' % ( self.user, self.serverIP )
132
        else:
133
            self.dest = None
134
        self.controlPath = controlPath
135
        self.sshcmd = []
136
        if self.dest:
137
            self.sshcmd = [ 'sudo', '-E', '-u', self.user ] + self.sshbase
138
            if self.controlPath:
139
                self.sshcmd += [ '-o', 'ControlPath=' + self.controlPath,
140
                                               '-o', 'ControlMaster=auto' ]
141
            self.sshcmd = self.sshcmd + [ self.dest ]
142
        self.splitInit = splitInit
143
        super( RemoteMixin, self ).__init__( name, **kwargs )
144

    
145
    @staticmethod
146
    def findUser():
147
        "Try to return logged-in (usually non-root) user"
148
        try:
149
            # If we're running sudo
150
            return os.environ[ 'SUDO_USER' ]
151
        except:
152
            try:
153
                # Logged-in user (if we have a tty)
154
                return quietRun( 'who am i' ).split()[ 0 ]
155
            except:
156
                # Give up and return effective user
157
                return quietRun( 'whoami' )
158

    
159
    # Determine IP address of local host
160
    _ipMatchRegex = re.compile( r'\d+\.\d+\.\d+\.\d+' )
161

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

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

    
192
    def finishInit( self ):
193
        self.pid = int( self.waitOutput() )
194

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

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

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

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

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

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

    
262

    
263
class RemoteNode( RemoteMixin, Node ):
264
    "A node on a remote server"
265
    pass
266

    
267

    
268
class RemoteHost( RemoteNode ):
269
    "A RemoteHost is simply a RemoteNode"
270
    pass
271

    
272

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

    
285

    
286

    
287
class RemoteLink( Link ):
288

    
289
    "A RemoteLink is a link between nodes which may be on different servers"
290

    
291
    def __init__( self, node1, node2, **kwargs ):
292
        """Initialize a RemoteLink
293
           see Link() for parameters"""
294
        # Create links on remote node
295
        self.node1 = node1
296
        self.node2 = node2
297
        self.tunnel = None
298
        kwargs.setdefault( 'params1', {} )
299
        kwargs.setdefault( 'params2', {} )
300
        Link.__init__( self, node1, node2, **kwargs )
301

    
302
    def stop( self ):
303
        "Stop this link"
304
        if self.tunnel:
305
            self.tunnel.terminate()
306
        self.tunnel = None
307

    
308
    def makeIntfPair( self, intfname1, intfname2, addr1=None, addr2=None ):
309
        """Create pair of interfaces
310
            intfname1: name of interface 1
311
            intfname2: name of interface 2
312
            (override this method [and possibly delete()]
313
            to change link type)"""
314
        node1, node2 = self.node1, self.node2
315
        server1 = getattr( node1, 'server', None )
316
        server2 = getattr( node2, 'server', None )
317
        if not server1 and not server2:
318
            # Local link
319
            return makeIntfPair( intfname1, intfname2, addr1, addr2 )
320
        elif server1 == server2:
321
            # Remote link on same remote server
322
            return makeIntfPair( intfname1, intfname2, addr1, addr2,
323
                                 run=node1.rcmd )
324
        # Otherwise, make a tunnel
325
        self.tunnel = self.makeTunnel( node1, node2, intfname1, intfname2, addr1, addr2 )
326
        return self.tunnel
327

    
328
    @staticmethod
329
    def moveIntf( intf, node, printError=True ):
330
        """Move remote interface from root ns to node
331
            intf: string, interface
332
            dstNode: destination Node
333
            srcNode: source Node or None (default) for root ns
334
            printError: if true, print error"""
335
        intf = str( intf )
336
        cmd = 'ip link set %s netns %s' % ( intf, node.pid )
337
        node.rcmd( cmd )
338
        links = node.cmd( 'ip link show' )
339
        if not ( ' %s:' % intf ) in links:
340
            if printError:
341
                error( '*** Error: RemoteLink.moveIntf: ' + intf +
342
                      ' not successfully moved to ' + node.name + '\n' )
343
            return False
344
        return True
345
    
346
    def makeTunnel( self, node1, node2, intfname1, intfname2,
347
                    addr1=None, addr2=None ):
348
        "Make a tunnel across switches on different servers"
349
        # 1. Create tap interfaces
350
        for node in node1, node2:
351
            # For now we are hard-wiring tap9, which we will rename
352
            node.rcmd( 'ip link delete tap9', stderr=PIPE )
353
            cmd = 'ip tuntap add dev tap9 mode tap user ' + node.user
354
            node.rcmd( cmd )
355
            links = node.rcmd( 'ip link show' )
356
            # print 'after add, links =', links
357
            assert 'tap9' in links
358
        # 2. Create ssh tunnel between tap interfaces
359
        # -n: close stdin
360
        dest = '%s@%s' % ( node2.user, node2.serverIP )
361
        cmd = [ 'ssh', '-n', '-o', 'Tunnel=Ethernet', '-w', '9:9',
362
                dest, 'echo @' ]
363
        self.cmd = cmd
364
        tunnel = node1.rpopen( cmd, sudo=False )
365
        # When we receive the character '@', it means that our
366
        # tunnel should be set up
367
        debug( 'Waiting for tunnel to come up...\n' )
368
        ch = tunnel.stdout.read( 1 )
369
        if ch != '@':
370
            error( 'makeTunnel:\n',
371
                   'Tunnel setup failed for',
372
                   '%s:%s' % ( node1, node1.dest ), 'to', 
373
                   '%s:%s\n' % ( node2, node2.dest ),
374
                  'command was:', cmd, '\n' )
375
            tunnel.terminate()
376
            tunnel.wait()
377
            error( ch + tunnel.stdout.read() )
378
            error( tunnel.stderr.read() )
379
            exit( 1 )
380
        # 3. Move interfaces if necessary
381
        for node in node1, node2:
382
            if node.inNamespace:
383
                retry( 3, .01, RemoteLink.moveIntf, 'tap9', node )
384
        # 4. Rename tap interfaces to desired names
385
        for node, intf, addr in ( ( node1, intfname1, addr1 ),
386
                            ( node2, intfname2, addr2 ) ):
387
            if not addr:
388
                node.cmd( 'ip link set tap9 name', intf )
389
            else:
390
                node.cmd( 'ip link set tap9 name', intf, 'address', addr )
391
        for node, intf in ( ( node1, intfname1 ), ( node2, intfname2 ) ):
392
            assert intf in node.cmd( 'ip link show' )
393
        return tunnel
394

    
395
    def status( self ):
396
        "Detailed representation of link"
397
        if self.tunnel:
398
            if self.tunnel.poll() is not None:
399
                status = "Tunnel EXITED %s" % self.tunnel.returncode
400
            else:
401
                status = "Tunnel Running (%s: %s)" % (
402
                    self.tunnel.pid, self.cmd )
403
        else:
404
            status = "OK"
405
        result = "%s %s" % ( Link.status( self ), status )
406
        return result
407

    
408

    
409
# Some simple placement algorithms for MininetCluster
410

    
411
class Placer( object ):
412
    "Node placement algorithm for MininetCluster"
413
    
414
    def __init__( self, servers=None, nodes=None, hosts=None,
415
                 switches=None, controllers=None, links=None ):
416
        """Initialize placement object
417
           servers: list of servers
418
           nodes: list of all nodes
419
           hosts: list of hosts
420
           switches: list of switches
421
           controllers: list of controllers
422
           links: list of links
423
           (all arguments are optional)
424
           returns: server"""
425
        self.servers = servers or []
426
        self.nodes = nodes or []
427
        self.hosts = hosts or []
428
        self.switches = switches or []
429
        self.controllers = controllers or []
430
        self.links = links or []
431

    
432
    def place( self, node ):
433
        "Return server for a given node"
434
        # Default placement: run locally
435
        return None
436

    
437

    
438
class RandomPlacer( Placer ):
439
    "Random placement"
440
    def place( self, nodename ):
441
        """Random placement function
442
            nodename: node name"""
443
        # This may be slow with lots of servers
444
        return self.servers[ randrange( 0, len( self.servers ) ) ]
445

    
446

    
447
class RoundRobinPlacer( Placer ):
448
    """Round-robin placement
449
       Note this will usually result in cross-server links between
450
       hosts and switches"""
451
    
452
    def __init__( self, *args, **kwargs ):
453
        Placer.__init__( self, *args, **kwargs )
454
        self.next = 0
455

    
456
    def place( self, nodename ):
457
        """Round-robin placement function
458
            nodename: node name"""
459
        # This may be slow with lots of servers
460
        server = self.servers[ self.next ]
461
        self.next = ( self.next + 1 ) % len( self.servers )
462
        return server
463

    
464

    
465
class SwitchBinPlacer( Placer ):
466
    """Place switches (and controllers) into evenly-sized bins,
467
       and attempt to co-locate hosts and switches"""
468

    
469
    def __init__( self, *args, **kwargs ):
470
        Placer.__init__( self, *args, **kwargs )
471
        # Easy lookup for servers and node sets
472
        self.servdict = dict( enumerate( self.servers ) )
473
        self.hset = frozenset( self.hosts )
474
        self.sset = frozenset( self.switches )
475
        self.cset = frozenset( self.controllers )
476
        # Server and switch placement indices
477
        self.placement =  self.calculatePlacement()
478

    
479
    @staticmethod
480
    def bin( nodes, servers ):
481
        "Distribute nodes evenly over servers"
482
        # Calculate base bin size
483
        nlen = len( nodes )
484
        slen = len( servers )
485
        # Basic bin size
486
        quotient = int( nlen / slen )
487
        binsizes = { server: quotient for server in servers }
488
        # Distribute remainder
489
        remainder = nlen % slen
490
        for server in servers[ 0 : remainder ]:
491
            binsizes[ server ] += 1
492
        # Create binsize[ server ] tickets for each server
493
        tickets = sum( [ binsizes[ server ] * [ server ]
494
                         for server in servers ], [] )
495
        # And assign one ticket to each node
496
        return { node: ticket for node, ticket in zip( nodes, tickets ) }
497

    
498
    def calculatePlacement( self ):
499
        "Pre-calculate node placement"
500
        placement = {}
501
        # Create host-switch connectivity map,
502
        # associating host with last switch that it's
503
        # connected to
504
        switchFor = {}
505
        for src, dst in self.links:
506
            if src in self.hset and dst in self.sset:
507
                switchFor[ src ] = dst
508
            if dst in self.hset and src in self.sset:
509
                switchFor[ dst ] = src
510
        # Place switches
511
        placement = self.bin( self.switches, self.servers )
512
        # Place controllers and merge into placement dict
513
        placement.update( self.bin( self.controllers, self.servers ) )
514
        # Co-locate hosts with their switches
515
        for h in self.hosts:
516
            if h in placement:
517
                # Host is already placed - leave it there
518
                continue
519
            if h in switchFor:
520
                placement[ h ] = placement[ switchFor[ h ] ]
521
            else:
522
                raise Exception(
523
                        "SwitchBinPlacer: cannot place isolated host " + h )
524
        return placement
525

    
526
    def place( self, node ):
527
        """Simple placement algorithm:
528
           place switches into evenly sized bins,
529
           and place hosts near their switches"""
530
        return self.placement[ node ]
531

    
532

    
533
class HostSwitchBinPlacer( Placer ):
534
    """Place switches *and hosts* into evenly-sized bins
535
       Note that this will usually result in cross-server
536
       links between hosts and switches"""
537

    
538
    def __init__( self, *args, **kwargs ):
539
        Placer.__init__( self, *args, **kwargs )
540
        # Calculate bin sizes
541
        scount = len( self.servers )
542
        self.hbin = max( int( len( self.hosts ) / scount ), 1 )
543
        self.sbin = max( int( len( self.switches ) / scount ), 1 )
544
        self.cbin = max( int( len( self.controllers ) / scount ) , 1 )
545
        info( 'scount:', scount )
546
        info( 'bins:', self.hbin, self.sbin, self.cbin, '\n' )
547
        self.servdict = dict( enumerate( self.servers ) )
548
        self.hset = frozenset( self.hosts )
549
        self.sset = frozenset( self.switches )
550
        self.cset = frozenset( self.controllers )
551
        self.hind, self.sind, self.cind = 0, 0, 0
552
    
553
    def place( self, nodename ):
554
        """Simple placement algorithm:
555
            place nodes into evenly sized bins"""
556
        # Place nodes into bins
557
        if nodename in self.hset:
558
            server = self.servdict[ self.hind / self.hbin ]
559
            self.hind += 1
560
        elif nodename in self.sset:
561
            server = self.servdict[ self.sind / self.sbin ]
562
            self.sind += 1
563
        elif nodename in self.cset:
564
            server = self.servdict[ self.cind / self.cbin ]
565
            self.cind += 1
566
        else:
567
            info( 'warning: unknown node', nodename )
568
            server = self.servdict[ 0 ]
569
        return server
570

    
571

    
572

    
573
# The MininetCluster class is not strictly necessary.
574
# However, it has several purposes:
575
# 1. To set up ssh connection sharing/multiplexing
576
# 2. To pre-flight the system so that everything is more likely to work
577
# 3. To allow connection/connectivity monitoring
578
# 4. To support pluggable placement algorithms
579

    
580
class MininetCluster( Mininet ):
581

    
582
    "Cluster-enhanced version of Mininet class"
583

    
584
    # Default ssh command
585
    # BatchMode yes: don't ask for password
586
    # ForwardAgent yes: forward authentication credentials
587
    sshcmd = [ 'ssh', '-o', 'BatchMode=yes', '-o', 'ForwardAgent=yes' ]
588

    
589
    def __init__( self, *args, **kwargs ):
590
        """servers: a list of servers to use (note: include
591
           localhost or None to use local system as well)
592
           user: user name for server ssh
593
           placement: Placer() subclass"""
594
        params = { 'host': RemoteHost,
595
                   'switch': RemoteOVSSwitch,
596
                   'link': RemoteLink,
597
                   'precheck': True }
598
        params.update( kwargs )
599
        servers = params.pop( 'servers', [ None ] )
600
        servers = [ s if s != 'localhost' else None for s in servers ]
601
        self.servers = servers
602
        self.serverIP = params.pop( 'serverIP', {} )
603
        if not self.serverIP:
604
            self.serverIP = { server: RemoteMixin.findServerIP( server )
605
                              for server in self.servers }
606
        self.user = params.pop( 'user', RemoteMixin.findUser() )
607
        if params.pop( 'precheck' ):
608
            self.precheck()
609
        self.connections = {}
610
        self.placement = params.pop( 'placement', SwitchBinPlacer )
611
        # Make sure control directory exists
612
        self.cdir = os.environ[ 'HOME' ] + '/.ssh/mn'
613
        errRun( [ 'mkdir', '-p', self.cdir ] )
614
        Mininet.__init__( self, *args, **params )
615

    
616
    def popen( self, cmd ):
617
        "Popen() for server connections"
618
        old = signal( SIGINT, SIG_IGN )
619
        conn = Popen( cmd, stdin=PIPE, stdout=PIPE, close_fds=True )
620
        signal( SIGINT, old )
621
        return conn
622

    
623
    def baddLink( self, *args, **kwargs ):
624
        "break addlink for testing"
625
        pass
626

    
627
    def precheck( self ):
628
        """Pre-check to make sure connection works and that
629
           we can call sudo without a password"""
630
        result = 0
631
        info( '*** Checking servers\n' )
632
        for server in self.servers:
633
            ip = self.serverIP[ server ]
634
            if not server or server == 'localhost':
635
                 continue
636
            info( server, '' )
637
            dest = '%s@%s' % ( self.user, ip )
638
            cmd = [ 'sudo', '-E', '-u', self.user ]
639
            cmd += self.sshcmd + [ '-n', dest, 'sudo true' ]
640
            debug( ' '.join( cmd ), '\n' )
641
            out, err, code = errRun( cmd )
642
            if code != 0:
643
                error( '\nstartConnection: server connection check failed '
644
                       'to %s using command:\n%s\n'
645
                        % ( server, ' '.join( cmd ) ) )
646
            result |= code
647
        if result:
648
            error( '*** Server precheck failed.\n'
649
                   '*** Make sure that the above ssh command works correctly.\n'
650
                   '*** You may also need to run mn -c on all nodes, and/or\n'
651
                   '*** use sudo -E.\n' )
652
            exit( 1 )
653
        info( '\n' )
654

    
655
    def modifiedaddHost( self, *args, **kwargs ):
656
        "Slightly modify addHost"
657
        kwargs[ 'splitInit' ] = True
658
        return Mininet.addHost( *args, **kwargs )
659

    
660

    
661
    def placeNodes( self ):
662
        """Place nodes on servers (if they don't have a server), and
663
           start shell processes"""
664
        if not self.servers or not self.topo:
665
            # No shirt, no shoes, no service
666
            return
667
        nodes = self.topo.nodes()
668
        placer = self.placement( servers=self.servers,
669
                                 nodes=self.topo.nodes(),
670
                                 hosts=self.topo.hosts(),
671
                                 switches=self.topo.switches(),
672
                                 links=self.topo.links() )
673
        for node in nodes:
674
            config = self.topo.node_info[ node ]
675
            server = config.setdefault( 'server', placer.place( node ) )
676
            if server:
677
                config.setdefault( 'serverIP', self.serverIP[ server ] )
678
            info( '%s:%s ' % ( node, server ) )
679
            key = ( None, server )
680
            _dest, cfile, _conn = self.connections.get(
681
                        key, ( None, None, None ) )
682
            if cfile:
683
                config.setdefault( 'controlPath', cfile )
684

    
685
    def addController( self, *args, **kwargs ):
686
        "Patch to update IP address to global IP address"
687
        controller = Mininet.addController( self, *args, **kwargs )
688
        # Update IP address for controller that may not be local
689
        if ( isinstance( controller, Controller)
690
             and controller.IP() == '127.0.0.1'
691
             and ' eth0:' in controller.cmd( 'ip link show' ) ):
692
             Intf( 'eth0', node=controller ).updateIP()
693
        return controller
694

    
695
    def buildFromTopo( self, *args, **kwargs ):
696
        "Start network"
697
        info( '*** Placing nodes\n' )
698
        self.placeNodes()
699
        info( '\n' )
700
        Mininet.buildFromTopo( self, *args, **kwargs )
701

    
702

    
703
def testNsTunnels():
704
    "Test tunnels between nodes in namespaces"
705
    net = Mininet( host=RemoteHost, link=RemoteLink )
706
    h1 = net.addHost( 'h1' )
707
    h2 = net.addHost( 'h2', server='ubuntu2' )
708
    net.addLink( h1, h2 )
709
    net.start()
710
    net.pingAll()
711
    net.stop()
712

    
713
# Manual topology creation with net.add*()
714
#
715
# This shows how node options may be used to manage
716
# cluster placement using the net.add*() API
717

    
718
def testRemoteNet( remote='ubuntu2' ):
719
    "Test remote Node classes"
720
    print '*** Remote Node Test'
721
    net = Mininet( host=RemoteHost, switch=RemoteOVSSwitch,
722
                   link=RemoteLink )
723
    c0 = net.addController( 'c0' )
724
    # Make sure controller knows its non-loopback address
725
    Intf( 'eth0', node=c0 ).updateIP()
726
    print "*** Creating local h1"
727
    h1 = net.addHost( 'h1' )
728
    print "*** Creating remote h2"
729
    h2 = net.addHost( 'h2', server=remote )
730
    print "*** Creating local s1"
731
    s1 = net.addSwitch( 's1' )
732
    print "*** Creating remote s2"
733
    s2 = net.addSwitch( 's2', server=remote )
734
    print "*** Adding links"
735
    net.addLink( h1, s1 )
736
    net.addLink( s1, s2 )
737
    net.addLink( h2, s2 )
738
    net.start()
739
    print 'Mininet is running on', quietRun( 'hostname' ).strip()
740
    for node in c0, h1, h2, s1, s2:
741
        print 'Node', node, 'is running on', node.cmd( 'hostname' ).strip()
742
    net.pingAll()
743
    CLI( net )
744
    net.stop()
745

    
746

    
747
# High-level/Topo API example
748
#
749
# This shows how existing Mininet topologies may be used in cluster
750
# mode by creating node placement functions and a controller which
751
# can be accessed remotely. This implements a very compatible version
752
# of cluster edition with a minimum of code!
753

    
754
remoteHosts = [ 'h2' ]
755
remoteSwitches = [ 's2' ]
756
remoteServer = 'ubuntu2'
757

    
758
def HostPlacer( name, *args, **params ):
759
    "Custom Host() constructor which places hosts on servers"
760
    if name in remoteHosts:
761
        return RemoteHost( name, *args, server=remoteServer, **params )
762
    else:
763
        return Host( name, *args, **params )
764

    
765
def SwitchPlacer( name, *args, **params ):
766
    "Custom Switch() constructor which places switches on servers"
767
    if name in remoteSwitches:
768
        return RemoteOVSSwitch( name, *args, server=remoteServer, **params )
769
    else:
770
        return RemoteOVSSwitch( name, *args, **params )
771

    
772
def ClusterController( *args, **kwargs):
773
    "Custom Controller() constructor which updates its eth0 IP address"
774
    controller = Controller( *args, **kwargs )
775
    # Find out its IP address so that cluster switches can connect
776
    Intf( 'eth0', node=controller ).updateIP()
777
    return controller
778

    
779
def testRemoteTopo():
780
    "Test remote Node classes using Mininet()/Topo() API"
781
    topo = LinearTopo( 2 )
782
    net = Mininet( topo=topo, host=HostPlacer, switch=SwitchPlacer,
783
                  link=RemoteLink, controller=ClusterController )
784
    net.start()
785
    net.pingAll()
786
    net.stop()
787

    
788
# Need to test backwards placement, where each host is on
789
# a server other than its switch!! But seriously we could just
790
# do random switch placement rather than completely random
791
# host placement.
792

    
793
def testRemoteSwitches():
794
    "Test with local hosts and remote switches"
795
    servers = [ 'localhost', 'ubuntu2']
796
    topo = TreeTopo( depth=4, fanout=2 )
797
    net = MininetCluster( topo=topo, servers=servers,
798
                          placement=RoundRobinPlacer )
799
    net.start()
800
    net.pingAll()
801
    net.stop()
802

    
803

    
804
#
805
# For testing and demo purposes it would be nice to draw the
806
# network graph and color it based on server.
807

    
808
# The MininetCluster() class integrates pluggable placement
809
# functions, for maximum ease of use. MininetCluster() also
810
# pre-flights and multiplexes server connections.
811

    
812
def testMininetCluster():
813
    "Test MininetCluster()"
814
    servers = [ 'localhost', 'ubuntu2' ]
815
    topo = TreeTopo( depth=3, fanout=3 )
816
    net = MininetCluster( topo=topo, servers=servers,
817
                          placement=SwitchBinPlacer )
818
    net.start()
819
    net.pingAll()
820
    net.stop()
821

    
822
def signalTest():
823
    "Make sure hosts are robust to signals"
824
    h = RemoteHost( 'h0', server='ubuntu1' )
825
    h.shell.send_signal( SIGINT )
826
    h.shell.poll()
827
    if h.shell.returncode is None:
828
        print 'OK: ', h, 'has not exited'
829
    else:
830
        print 'FAILURE:', h, 'exited with code', h.shell.returncode
831
    h.stop()
832

    
833
if __name__ == '__main__':
834
    setLogLevel( 'info' )
835
    # testRemoteTopo()
836
    # testRemoteNet()
837
    # testMininetCluster()
838
    # testRemoteSwitches()
839
    signalTest()