Statistics
| Branch: | Tag: | Revision:

mininet / examples / cluster.py @ b1ec912d

History | View | Annotate | Download (30.5 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 else self.findServerIP( self.server )
127
        self.user = user if user else self.findUser()
128
        if controlPath is True:
129
            # Set a default control path for shared SSH connections
130
            controlPath = '/tmp/mn-%r@%h:%p'
131
        self.controlPath = controlPath
132
        self.splitInit = splitInit
133
        if self.user and self.server != 'localhost':
134
            self.dest = '%s@%s' % ( self.user, self.serverIP )
135
            self.sshcmd = [ 'sudo', '-E', '-u', self.user ] + self.sshbase
136
            if self.controlPath:
137
                self.sshcmd += [ '-o', 'ControlPath=' + self.controlPath,
138
                                 '-o', 'ControlMaster=auto',
139
                                 '-o', 'ControlPersist=' + '1' ]
140
            self.sshcmd = self.sshcmd + [ self.dest ]
141
            self.isRemote = True
142
        else:
143
            self.dest = None
144
            self.sshcmd = []
145
            self.isRemote = False
146
        super( RemoteMixin, self ).__init__( name, **kwargs )
147

    
148
    @staticmethod
149
    def findUser():
150
        "Try to return logged-in (usually non-root) user"
151
        try:
152
            # If we're running sudo
153
            return os.environ[ 'SUDO_USER' ]
154
        except:
155
            try:
156
                # Logged-in user (if we have a tty)
157
                return quietRun( 'who am i' ).split()[ 0 ]
158
            except:
159
                # Give up and return effective user
160
                return 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
        self.pid = int( self.waitOutput() )
191

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

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

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

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

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

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

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

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

    
271

    
272
class RemoteHost( RemoteNode ):
273
    "A RemoteHost is simply a RemoteNode"
274
    pass
275

    
276

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

    
289

    
290

    
291
class RemoteLink( Link ):
292

    
293
    "A RemoteLink is a link between nodes which may be on different servers"
294

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

    
306
    def stop( self ):
307
        "Stop this link"
308
        if self.tunnel:
309
            self.tunnel.terminate()
310
        self.tunnel = None
311

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

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

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

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

    
419

    
420
# Some simple placement algorithms for MininetCluster
421

    
422
class Placer( object ):
423
    "Node placement algorithm for MininetCluster"
424

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

    
443
    def place( self, node ):
444
        "Return server for a given node"
445
        # Default placement: run locally
446
        return None
447

    
448

    
449
class RandomPlacer( Placer ):
450
    "Random placement"
451
    def place( self, nodename ):
452
        """Random placement function
453
            nodename: node name"""
454
        # This may be slow with lots of servers
455
        return self.servers[ randrange( 0, len( self.servers ) ) ]
456

    
457

    
458
class RoundRobinPlacer( Placer ):
459
    """Round-robin placement
460
       Note this will usually result in cross-server links between
461
       hosts and switches"""
462

    
463
    def __init__( self, *args, **kwargs ):
464
        Placer.__init__( self, *args, **kwargs )
465
        self.next = 0
466

    
467
    def place( self, nodename ):
468
        """Round-robin placement function
469
            nodename: node name"""
470
        # This may be slow with lots of servers
471
        server = self.servers[ self.next ]
472
        self.next = ( self.next + 1 ) % len( self.servers )
473
        return server
474

    
475

    
476
class SwitchBinPlacer( Placer ):
477
    """Place switches (and controllers) into evenly-sized bins,
478
       and attempt to co-locate hosts and switches"""
479

    
480
    def __init__( self, *args, **kwargs ):
481
        Placer.__init__( self, *args, **kwargs )
482
        # Easy lookup for servers and node sets
483
        self.servdict = dict( enumerate( self.servers ) )
484
        self.hset = frozenset( self.hosts )
485
        self.sset = frozenset( self.switches )
486
        self.cset = frozenset( self.controllers )
487
        # Server and switch placement indices
488
        self.placement =  self.calculatePlacement()
489

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

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

    
537
    def place( self, node ):
538
        """Simple placement algorithm:
539
           place switches into evenly sized bins,
540
           and place hosts near their switches"""
541
        return self.placement[ node ]
542

    
543

    
544
class HostSwitchBinPlacer( Placer ):
545
    """Place switches *and hosts* into evenly-sized bins
546
       Note that this will usually result in cross-server
547
       links between hosts and switches"""
548

    
549
    def __init__( self, *args, **kwargs ):
550
        Placer.__init__( self, *args, **kwargs )
551
        # Calculate bin sizes
552
        scount = len( self.servers )
553
        self.hbin = max( int( len( self.hosts ) / scount ), 1 )
554
        self.sbin = max( int( len( self.switches ) / scount ), 1 )
555
        self.cbin = max( int( len( self.controllers ) / scount ) , 1 )
556
        info( 'scount:', scount )
557
        info( 'bins:', self.hbin, self.sbin, self.cbin, '\n' )
558
        self.servdict = dict( enumerate( self.servers ) )
559
        self.hset = frozenset( self.hosts )
560
        self.sset = frozenset( self.switches )
561
        self.cset = frozenset( self.controllers )
562
        self.hind, self.sind, self.cind = 0, 0, 0
563

    
564
    def place( self, nodename ):
565
        """Simple placement algorithm:
566
            place nodes into evenly sized bins"""
567
        # Place nodes into bins
568
        if nodename in self.hset:
569
            server = self.servdict[ self.hind / self.hbin ]
570
            self.hind += 1
571
        elif nodename in self.sset:
572
            server = self.servdict[ self.sind / self.sbin ]
573
            self.sind += 1
574
        elif nodename in self.cset:
575
            server = self.servdict[ self.cind / self.cbin ]
576
            self.cind += 1
577
        else:
578
            info( 'warning: unknown node', nodename )
579
            server = self.servdict[ 0 ]
580
        return server
581

    
582

    
583

    
584
# The MininetCluster class is not strictly necessary.
585
# However, it has several purposes:
586
# 1. To set up ssh connection sharing/multiplexing
587
# 2. To pre-flight the system so that everything is more likely to work
588
# 3. To allow connection/connectivity monitoring
589
# 4. To support pluggable placement algorithms
590

    
591
class MininetCluster( Mininet ):
592

    
593
    "Cluster-enhanced version of Mininet class"
594

    
595
    # Default ssh command
596
    # BatchMode yes: don't ask for password
597
    # ForwardAgent yes: forward authentication credentials
598
    sshcmd = [ 'ssh', '-o', 'BatchMode=yes', '-o', 'ForwardAgent=yes' ]
599

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

    
627
    def popen( self, cmd ):
628
        "Popen() for server connections"
629
        old = signal( SIGINT, SIG_IGN )
630
        conn = Popen( cmd, stdin=PIPE, stdout=PIPE, close_fds=True )
631
        signal( SIGINT, old )
632
        return conn
633

    
634
    def baddLink( self, *args, **kwargs ):
635
        "break addlink for testing"
636
        pass
637

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

    
666
    def modifiedaddHost( self, *args, **kwargs ):
667
        "Slightly modify addHost"
668
        kwargs[ 'splitInit' ] = True
669
        return Mininet.addHost( *args, **kwargs )
670

    
671

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

    
699
    def addController( self, *args, **kwargs ):
700
        "Patch to update IP address to global IP address"
701
        controller = Mininet.addController( self, *args, **kwargs )
702
        # Update IP address for controller that may not be local
703
        if ( isinstance( controller, Controller)
704
             and controller.IP() == '127.0.0.1'
705
             and ' eth0:' in controller.cmd( 'ip link show' ) ):
706
            Intf( 'eth0', node=controller ).updateIP()
707
        return controller
708

    
709
    def buildFromTopo( self, *args, **kwargs ):
710
        "Start network"
711
        info( '*** Placing nodes\n' )
712
        self.placeNodes()
713
        info( '\n' )
714
        Mininet.buildFromTopo( self, *args, **kwargs )
715

    
716

    
717
def testNsTunnels():
718
    "Test tunnels between nodes in namespaces"
719
    net = Mininet( host=RemoteHost, link=RemoteLink )
720
    h1 = net.addHost( 'h1' )
721
    h2 = net.addHost( 'h2', server='ubuntu2' )
722
    net.addLink( h1, h2 )
723
    net.start()
724
    net.pingAll()
725
    net.stop()
726

    
727
# Manual topology creation with net.add*()
728
#
729
# This shows how node options may be used to manage
730
# cluster placement using the net.add*() API
731

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

    
760

    
761
# High-level/Topo API example
762
#
763
# This shows how existing Mininet topologies may be used in cluster
764
# mode by creating node placement functions and a controller which
765
# can be accessed remotely. This implements a very compatible version
766
# of cluster edition with a minimum of code!
767

    
768
remoteHosts = [ 'h2' ]
769
remoteSwitches = [ 's2' ]
770
remoteServer = 'ubuntu2'
771

    
772
def HostPlacer( name, *args, **params ):
773
    "Custom Host() constructor which places hosts on servers"
774
    if name in remoteHosts:
775
        return RemoteHost( name, *args, server=remoteServer, **params )
776
    else:
777
        return Host( name, *args, **params )
778

    
779
def SwitchPlacer( name, *args, **params ):
780
    "Custom Switch() constructor which places switches on servers"
781
    if name in remoteSwitches:
782
        return RemoteOVSSwitch( name, *args, server=remoteServer, **params )
783
    else:
784
        return RemoteOVSSwitch( name, *args, **params )
785

    
786
def ClusterController( *args, **kwargs):
787
    "Custom Controller() constructor which updates its eth0 IP address"
788
    controller = Controller( *args, **kwargs )
789
    # Find out its IP address so that cluster switches can connect
790
    Intf( 'eth0', node=controller ).updateIP()
791
    return controller
792

    
793
def testRemoteTopo():
794
    "Test remote Node classes using Mininet()/Topo() API"
795
    topo = LinearTopo( 2 )
796
    net = Mininet( topo=topo, host=HostPlacer, switch=SwitchPlacer,
797
                  link=RemoteLink, controller=ClusterController )
798
    net.start()
799
    net.pingAll()
800
    net.stop()
801

    
802
# Need to test backwards placement, where each host is on
803
# a server other than its switch!! But seriously we could just
804
# do random switch placement rather than completely random
805
# host placement.
806

    
807
def testRemoteSwitches():
808
    "Test with local hosts and remote switches"
809
    servers = [ 'localhost', 'ubuntu2']
810
    topo = TreeTopo( depth=4, fanout=2 )
811
    net = MininetCluster( topo=topo, servers=servers,
812
                          placement=RoundRobinPlacer )
813
    net.start()
814
    net.pingAll()
815
    net.stop()
816

    
817

    
818
#
819
# For testing and demo purposes it would be nice to draw the
820
# network graph and color it based on server.
821

    
822
# The MininetCluster() class integrates pluggable placement
823
# functions, for maximum ease of use. MininetCluster() also
824
# pre-flights and multiplexes server connections.
825

    
826
def testMininetCluster():
827
    "Test MininetCluster()"
828
    servers = [ 'localhost', 'ubuntu2' ]
829
    topo = TreeTopo( depth=3, fanout=3 )
830
    net = MininetCluster( topo=topo, servers=servers,
831
                          placement=SwitchBinPlacer )
832
    net.start()
833
    net.pingAll()
834
    net.stop()
835

    
836
def signalTest():
837
    "Make sure hosts are robust to signals"
838
    h = RemoteHost( 'h0', server='ubuntu1' )
839
    h.shell.send_signal( SIGINT )
840
    h.shell.poll()
841
    if h.shell.returncode is None:
842
        print 'OK: ', h, 'has not exited'
843
    else:
844
        print 'FAILURE:', h, 'exited with code', h.shell.returncode
845
    h.stop()
846

    
847
if __name__ == '__main__':
848
    setLogLevel( 'info' )
849
    # testRemoteTopo()
850
    # testRemoteNet()
851
    # testMininetCluster()
852
    # testRemoteSwitches()
853
    signalTest()