4 cluster.py: prototyping/experimentation for distributed Mininet,
5 aka Mininet: Cluster Edition
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()
16 These are largely interoperable with local objects.
18 - One Mininet to rule them all
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.
23 - Multiple placement models
25 Placement should be as easy as possible. We should provide basic placement
26 support and also allow for explicit placement.
30 What is the basic communication mechanism?
32 To start with? Probably a single multiplexed ssh connection between each
33 pair of mininet servers that needs to communicate.
35 How are tunnels created?
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.
42 How are tunnels destroyed?
44 They are destroyed when the links are deleted in Mininet.stop()
46 How does RemoteNode.popen() work?
48 It opens a shared ssh connection to the remote server and attaches to
49 the namespace using mnexec -a -g.
51 Is there any value to using Paramiko vs. raw ssh?
53 Maybe, but it doesn't seem to support L2 tunneling.
55 Should we preflight the entire network, including all server-to-server
58 Yes! We don't yet do this with remote server-to-server connections yet.
60 Should we multiplex the link ssh connections?
62 Yes, this is done automatically with ControlMaster=auto.
65 Please add UseDNS: no to your /etc/ssh/sshd_config!!!
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)
78 from mininet.node import Node, Host, OVSSwitch, Controller
79 from mininet.link import Link, Intf
80 from mininet.net import Mininet
81 from mininet.topo import LinearTopo
82 from mininet.topolib import TreeTopo
83 from mininet.util import quietRun, errRun
84 from mininet.examples.clustercli import CLI
85 from mininet.log import setLogLevel, debug, info, error
86 from mininet.clean import addCleanupCallback
88 from signal import signal, SIGINT, SIG_IGN
89 from subprocess import Popen, PIPE, STDOUT
91 from random import randrange
94 from itertools import groupby
95 from operator import attrgetter
96 from distutils.version import StrictVersion
100 "Try to return logged-in (usually non-root) user"
102 # If we're running sudo
103 os.environ.get( 'SUDO_USER', False ) or
104 # Logged-in user (if we have a tty)
105 ( quietRun( 'who am i' ).split() or [ False ] )[ 0 ] or
106 # Give up and return effective user
107 quietRun( 'whoami' ).strip() )
110 class ClusterCleanup( object ):
117 def add( cls, server, user='' ):
118 "Add an entry to server: user dict"
120 addCleanupCallback( cls.cleanup )
123 cls.serveruser[ server ] = user
128 info( '***Limpiando todo...\n' )
129 for server, user in cls.serveruser.items():
130 if server == 'localhost':
131 # Handled by mininet.clean.cleanup()
134 cmd = [ 'su', user, '-c',
135 'ssh %s@%s sudo mn -c' % ( user, server ) ]
137 info( quietRun( cmd ) )
139 # BL note: so little code is required for remote nodes,
140 # we will probably just want to update the main Node()
141 # class to enable it for remote access! However, there
142 # are a large number of potential failure conditions with
143 # remote nodes which we may want to detect and handle.
144 # Another interesting point is that we could put everything
145 # in a mix-in class and easily add cluster mode to 2.0.
147 class RemoteMixin( object ):
149 "A mix-in class to turn local nodes into remote nodes"
152 # -q: don't print stupid diagnostic messages
153 # BatchMode yes: don't ask for password
154 # ForwardAgent yes: forward authentication credentials
155 sshbase = [ 'ssh', '-q',
156 '-o', 'BatchMode=yes',
157 '-o', 'ForwardAgent=yes', '-tt' ]
159 def __init__( self, name, server='localhost', user=None, serverIP=None,
160 controlPath=False, splitInit=False, **kwargs):
161 """Instantiate a remote node
162 name: name of remote node
163 server: remote server (optional)
164 user: user on remote server (optional)
165 controlPath: specify shared ssh control path (optional)
166 splitInit: split initialization?
167 **kwargs: see Node()"""
168 # We connect to servers by IP address
169 self.server = server if server else 'localhost'
170 self.serverIP = ( serverIP if serverIP
171 else self.findServerIP( self.server ) )
172 self.user = user if user else findUser()
173 ClusterCleanup.add( server=server, user=user )
174 if controlPath is True:
175 # Set a default control path for shared SSH connections
176 controlPath = '/tmp/mn-%r@%h:%p'
177 self.controlPath = controlPath
178 self.splitInit = splitInit
179 if self.user and self.server != 'localhost':
180 self.dest = '%s@%s' % ( self.user, self.serverIP )
181 self.sshcmd = [ 'sudo', '-E', '-u', self.user ] + self.sshbase
183 self.sshcmd += [ '-o', 'ControlPath=' + self.controlPath,
184 '-o', 'ControlMaster=auto',
185 '-o', 'ControlPersist=' + '1' ]
186 self.sshcmd += [ self.dest ]
191 self.isRemote = False
193 self.shell, self.pid = None, None
194 super( RemoteMixin, self ).__init__( name, **kwargs )
196 # Determine IP address of local host
197 _ipMatchRegex = re.compile( r'\d+\.\d+\.\d+\.\d+' )
200 def findServerIP( cls, server ):
201 "Return our server's IP address"
202 # First, check for an IP address
203 ipmatch = cls._ipMatchRegex.findall( server )
206 # Otherwise, look up remote server
207 output = quietRun( 'getent ahostsv4 %s' % server )
208 ips = cls._ipMatchRegex.findall( output )
209 ip = ips[ 0 ] if ips else None
212 # Command support via shell process in namespace
213 def startShell( self, *args, **kwargs ):
214 "Start a shell process for running commands"
216 kwargs.update( mnopts='-c' )
217 super( RemoteMixin, self ).startShell( *args, **kwargs )
218 # Optional split initialization
219 self.sendCmd( 'echo $$' )
220 if not self.splitInit:
223 def finishInit( self ):
224 "Wait for split initialization to complete"
225 self.pid = int( self.waitOutput() )
227 def rpopen( self, *cmd, **opts ):
228 "Return a Popen object on underlying server in root namespace"
229 params = { 'stdin': PIPE,
233 params.update( opts )
234 return self._popen( *cmd, **params )
236 def rcmd( self, *cmd, **opts):
237 """rcmd: run a command on underlying server
239 args: string or list of strings
240 returns: stdout and stderr"""
241 popen = self.rpopen( *cmd, **opts )
242 # info( 'RCMD: POPEN:', popen, '\n' )
243 # These loops are tricky to get right.
244 # Once the process exits, we can read
245 # EOF twice if necessary.
249 result += popen.stdout.read()
256 "Detach from process group to ignore all signals"
259 def _popen( self, cmd, sudo=True, tt=True, **params):
260 """Spawn a process on a remote node
261 cmd: remote command to run (list)
262 **params: parameters to Popen()
263 returns: Popen() object"""
264 if type( cmd ) is str:
268 cmd = [ 'sudo', '-E' ] + cmd
270 cmd = self.sshcmd + cmd
273 sshcmd = list( self.sshcmd )
274 sshcmd.remove( '-tt' )
277 if self.user and not sudo:
279 cmd = [ 'sudo', '-E', '-u', self.user ] + cmd
280 params.update( preexec_fn=self._ignoreSignal )
281 debug( '_popen', cmd, '\n' )
282 popen = super( RemoteMixin, self )._popen( cmd, **params )
285 def popen( self, *args, **kwargs ):
286 "Override: disable -tt"
287 return super( RemoteMixin, self).popen( *args, tt=False, **kwargs )
289 def addIntf( self, *args, **kwargs ):
290 "Override: use RemoteLink.moveIntf"
291 # kwargs.update( moveIntfFn=RemoteLink.moveIntf )
292 return super( RemoteMixin, self).addIntf( *args, **kwargs )
295 class RemoteNode( RemoteMixin, Node ):
296 "A node on a remote server"
300 class RemoteHost( RemoteNode ):
301 "A RemoteHost is simply a RemoteNode"
305 class RemoteOVSSwitch( RemoteMixin, OVSSwitch ):
306 "Remote instance of Open vSwitch"
310 def __init__( self, *args, **kwargs ):
311 # No batch startup yet
312 kwargs.update( batch=True )
313 super( RemoteOVSSwitch, self ).__init__( *args, **kwargs )
315 def isOldOVS( self ):
316 "Is remote switch using an old OVS version?"
318 if self.server not in cls.OVSVersions:
319 # pylint: disable=not-callable
320 vers = self.cmd( 'ovs-vsctl --version' )
321 # pylint: enable=not-callable
322 cls.OVSVersions[ self.server ] = re.findall(
323 r'\d+\.\d+', vers )[ 0 ]
324 return ( StrictVersion( cls.OVSVersions[ self.server ] ) <
325 StrictVersion( '1.10' ) )
328 def batchStartup( cls, switches, **_kwargs ):
329 "Start up switches in per-server batches"
330 key = attrgetter( 'server' )
331 for server, switchGroup in groupby( sorted( switches, key=key ), key ):
332 info( '(%s)' % server )
333 group = tuple( switchGroup )
335 OVSSwitch.batchStartup( group, run=switch.cmd )
339 def batchShutdown( cls, switches, **_kwargs ):
340 "Stop switches in per-server batches"
341 key = attrgetter( 'server' )
342 for server, switchGroup in groupby( sorted( switches, key=key ), key ):
343 info( '(%s)' % server )
344 group = tuple( switchGroup )
346 OVSSwitch.batchShutdown( group, run=switch.rcmd )
350 class RemoteLink( Link ):
351 "A RemoteLink is a link between nodes which may be on different servers (SSH Link)"
353 def __init__( self, node1, node2, **kwargs ):
354 """Initialize a RemoteLink
355 see Link() for parameters"""
356 # Create links on remote node
360 kwargs.setdefault( 'params1', {} )
361 kwargs.setdefault( 'params2', {} )
362 self.cmd = None # satisfy pylint
363 Link.__init__( self, node1, node2, **kwargs )
368 self.tunnel.terminate()
375 def makeIntfPair( self, intfname1, intfname2, addr1=None, addr2=None,
376 node1=None, node2=None, deleteIntfs=True ):
377 """Create pair of interfaces
378 intfname1: name of interface 1
379 intfname2: name of interface 2
380 (override this method [and possibly delete()]
381 to change link type)"""
382 node1 = self.node1 if node1 is None else node1
383 node2 = self.node2 if node2 is None else node2
384 server1 = getattr( node1, 'server', 'localhost' ) #Obtiene el valor de server y localhost del objeto node1
385 server2 = getattr( node2, 'server', 'localhost' )
386 if server1 == server2:
387 # Link within same server
388 return Link.makeIntfPair( intfname1, intfname2, addr1, addr2,
389 node1, node2, deleteIntfs=deleteIntfs )
390 # Otherwise, make a tunnel
391 self.tunnel = self.makeTunnel( node1, node2, intfname1, intfname2,
396 def moveIntf( intf, node ):
397 """Move remote interface from root ns to node
398 intf: string, interface
399 dstNode: destination Node
400 srcNode: source Node or None (default) for root ns"""
402 cmd = 'ip link set %s netns %s' % ( intf, node.pid )
403 result = node.rcmd( cmd )
405 raise Exception('error executing command %s' % cmd)
408 def makeTunnel( self, node1, node2, intfname1, intfname2,
409 addr1=None, addr2=None ):
410 "Make a tunnel across switches on different servers"
411 # We should never try to create a tunnel to ourselves!
412 assert node1.server != node2.server
413 # And we can't ssh into this server remotely as 'localhost',
414 # so try again swappping node1 and node2
415 if node2.server == 'localhost':
416 return self.makeTunnel( node2, node1, intfname2, intfname1,
418 debug( '\n*** Make SSH tunnel ' + node1.server + ':' + intfname1 +
419 ' == ' + node2.server + ':' + intfname2 )
420 # 1. Create tap interfaces
421 for node in node1, node2:
422 # For now we are hard-wiring tap9, which we will rename
423 cmd = 'ip tuntap add dev tap9 mode tap user ' + node.user
424 result = node.rcmd( cmd )
426 raise Exception( 'error creating tap9 on %s: %s' %
428 # 2. Create ssh tunnel between tap interfaces
430 # ssh -w: crea un tunel entre local:dest. Those interfaces may be numerical ID or "any"
431 dest = '%s@%s' % ( node2.user, node2.serverIP )
432 cmd = [ 'ssh', '-n', '-o', 'Tunnel=Ethernet', '-w', '9:9',
435 tunnel = node1.rpopen( cmd, sudo=False )
436 # When we receive the character '@', it means that our
437 # tunnel should be set up
438 debug( '\n Waiting for tunnel to come up...\n' )
439 ch = tunnel.stdout.read( 1 )
441 raise Exception( 'makeTunnel:\n',
442 'Tunnel setup failed for',
443 '%s:%s' % ( node1, node1.dest ), 'to',
444 '%s:%s\n' % ( node2, node2.dest ),
445 'command was:', cmd, '\n' )
446 # 3. Move interfaces if necessary
447 for node in node1, node2:
448 if not self.moveIntf( 'tap9', node ):
449 raise Exception( 'interface move failed on node %s' % node )
450 # 4. Rename tap interfaces to desired names
451 for node, intf, addr in ( ( node1, intfname1, addr1 ),
452 ( node2, intfname2, addr2 ) ):
454 node.cmd( 'sudo ip link set tap9 down' ) #personalizada
455 result = node.cmd( 'sudo ip link set tap9 name', intf ) #he hecho un cambio agregando sudo
456 node.cmd( 'sudo ip link set', intf, 'up' ) #personalizada
458 node.cmd( 'sudo ip link set tap9 down' ) #personalizada
459 result = node.cmd( 'sudo ip link set tap9 name', intf,
461 node.cmd( 'sudo ip link set', intf, 'up') #personalizada
463 raise Exception( 'error renaming %s: %s' % ( intf, result ) )
467 "Detailed representation of link"
469 if self.tunnel.poll() is not None:
470 status = "Tunnel EXITED %s" % self.tunnel.returncode
472 status = "Tunnel Running (%s: %s)" % (
473 self.tunnel.pid, self.cmd )
476 result = "%s %s" % ( Link.status( self ), status )
480 class RemoteSSHLink( RemoteLink ):
481 "Remote link using SSH tunnels"
482 def __init__(self, node1, node2, **kwargs):
483 RemoteLink.__init__( self, node1, node2, **kwargs )
486 class RemoteGRELink( RemoteLink ):
487 "Remote link using GRE tunnels"
491 def __init__(self, node1, node2, **kwargs):
492 RemoteLink.__init__( self, node1, node2, **kwargs )
503 def makeIntfPair( self, intfname1, intfname2, addr1=None, addr2=None,
504 node1=None, node2=None, deleteIntfs=True ):
505 """Create pair of interfaces
506 intfname1: name of interface 1
507 intfname2: name of interface 2
508 (override this method [and possibly delete()]
509 to change link type)"""
510 node1 = self.node1 if node1 is None else node1
511 node2 = self.node2 if node2 is None else node2
512 server1 = getattr( node1, 'server', 'localhost' )
513 server2 = getattr( node2, 'server', 'localhost' )
514 if server1 == server2:
515 # Link within same server
516 Link.makeIntfPair( intfname1, intfname2, addr1, addr2,
517 node1, node2, deleteIntfs=deleteIntfs )
518 # Need to reduce the MTU of all emulated hosts to 1450 for GRE
519 # tunneling, otherwise packets larger than 1400 bytes cannot be
520 # successfully transmitted through the tunnel.
521 node1.cmd('ip link set dev %s mtu 1450' % intfname1)
522 node2.cmd('ip link set dev %s mtu 1450' % intfname2)
524 # Otherwise, make a tunnel
525 self.makeTunnel( node1, node2, intfname1, intfname2, addr1, addr2 )
528 def makeTunnel(self, node1, node2, intfname1, intfname2,
529 addr1=None, addr2=None):
530 "Make a tunnel across switches on different servers"
531 # We should never try to create a tunnel to ourselves!
532 assert node1.server != node2.server
533 if node2.server == 'localhost':
534 return self.makeTunnel( node2, node1, intfname2, intfname1,
536 IP1, IP2 = node1.serverIP, node2.serverIP
537 # GRE tunnel needs to be set up with the IP of the local interface
538 # that connects the remote node, NOT '127.0.0.1' of localhost
539 if node1.server == 'localhost':
540 output = quietRun('ip route get %s' % node2.serverIP)
541 IP1 = output.split(' src ')[1].split()[0]
542 debug( '\n*** Make GRE tunnel ' + node1.server + ':' + intfname1 +
543 ' == ' + node2.server + ':' + intfname2 )
544 tun1 = 'local ' + IP1 + ' remote ' + IP2
545 tun2 = 'local ' + IP2 + ' remote ' + IP1
546 self.__class__.GRE_KEY += 1
547 for (node, intfname, addr, tun) in [(node1, intfname1, addr1, tun1),
548 (node2, intfname2, addr2, tun2)]:
549 node.rcmd('ip link delete ' + intfname)
550 result = node.rcmd('ip link add name ' + intfname + ' type gretap '
551 + tun + ' ttl 64 key '
552 + str( self.__class__.GRE_KEY) )
554 raise Exception('error creating gretap on %s: %s'
557 node.rcmd('ip link set %s address %s' % (intfname, addr))
559 node.rcmd('ip link set dev %s up' % intfname)
560 node.rcmd('ip link set dev %s mtu 1450' % intfname)
561 if not self.moveIntf(intfname, node):
562 raise Exception('interface move failed on node %s' % node)
565 # Some simple placement algorithms for MininetCluster
567 class Placer( object ):
568 "Node placement algorithm for MininetCluster"
570 def __init__( self, servers=None, nodes=None, hosts=None,
571 switches=None, controllers=None, links=None ):
572 """Initialize placement object
573 servers: list of servers
574 nodes: list of all nodes
576 switches: list of switches
577 controllers: list of controllers
579 (all arguments are optional)
581 self.servers = servers or []
582 self.nodes = nodes or []
583 self.hosts = hosts or []
584 self.switches = switches or []
585 self.controllers = controllers or []
586 self.links = links or []
588 def place( self, node ):
589 "Return server for a given node"
590 assert self, node # satisfy pylint
591 # Default placement: run locally
595 class RandomPlacer( Placer ):
597 def place( self, nodename ):
598 """Random placement function
599 nodename: node name"""
600 assert nodename # please pylint
601 # This may be slow with lots of servers
602 return self.servers[ randrange( 0, len( self.servers ) ) ]
605 class RoundRobinPlacer( Placer ):
606 """Round-robin placement
607 Note this will usually result in cross-server links between
608 hosts and switches"""
610 def __init__( self, *args, **kwargs ):
611 Placer.__init__( self, *args, **kwargs )
614 def place( self, nodename ):
615 """Round-robin placement function
616 nodename: node name"""
617 assert nodename # please pylint
618 # This may be slow with lots of servers
619 server = self.servers[ self.next ]
620 self.next = ( self.next + 1 ) % len( self.servers )
624 class SwitchBinPlacer( Placer ):
625 """Place switches (and controllers) into evenly-sized bins,
626 and attempt to co-locate hosts and switches"""
628 def __init__( self, *args, **kwargs ):
629 Placer.__init__( self, *args, **kwargs )
630 # Easy lookup for servers and node sets
631 self.servdict = dict( enumerate( self.servers ) )
632 self.hset = frozenset( self.hosts )
633 self.sset = frozenset( self.switches )
634 self.cset = frozenset( self.controllers )
635 # Server and switch placement indices
636 self.placement = self.calculatePlacement()
639 def bin( nodes, servers ):
640 "Distribute nodes evenly over servers"
641 # Calculate base bin size
643 slen = len( servers )
645 quotient = int( nlen / slen )
646 binsizes = { server: quotient for server in servers }
647 # Distribute remainder
648 remainder = nlen % slen
649 for server in servers[ 0 : remainder ]:
650 binsizes[ server ] += 1
651 # Create binsize[ server ] tickets for each server
652 tickets = sum( [ binsizes[ server ] * [ server ]
653 for server in servers ], [] )
654 # And assign one ticket to each node
655 return { node: ticket for node, ticket in zip( nodes, tickets ) }
657 def calculatePlacement( self ):
658 "Pre-calculate node placement"
660 # Create host-switch connectivity map,
661 # associating host with last switch that it's
664 for src, dst in self.links:
665 if src in self.hset and dst in self.sset:
666 switchFor[ src ] = dst
667 if dst in self.hset and src in self.sset:
668 switchFor[ dst ] = src
670 placement = self.bin( self.switches, self.servers )
671 # Place controllers and merge into placement dict
672 placement.update( self.bin( self.controllers, self.servers ) )
673 # Co-locate hosts with their switches
676 # Host is already placed - leave it there
679 placement[ h ] = placement[ switchFor[ h ] ]
682 "SwitchBinPlacer: cannot place isolated host " + h )
685 def place( self, node ):
686 """Simple placement algorithm:
687 place switches into evenly sized bins,
688 and place hosts near their switches"""
689 return self.placement[ node ]
692 class HostSwitchBinPlacer( Placer ):
693 """Place switches *and hosts* into evenly-sized bins
694 Note that this will usually result in cross-server
695 links between hosts and switches"""
697 def __init__( self, *args, **kwargs ):
698 Placer.__init__( self, *args, **kwargs )
699 # Calculate bin sizes
700 scount = len( self.servers )
701 self.hbin = max( int( len( self.hosts ) / scount ), 1 )
702 self.sbin = max( int( len( self.switches ) / scount ), 1 )
703 self.cbin = max( int( len( self.controllers ) / scount ), 1 )
704 info( 'scount:', scount )
705 info( 'bins:', self.hbin, self.sbin, self.cbin, '\n' )
706 self.servdict = dict( enumerate( self.servers ) )
707 self.hset = frozenset( self.hosts )
708 self.sset = frozenset( self.switches )
709 self.cset = frozenset( self.controllers )
710 self.hind, self.sind, self.cind = 0, 0, 0
712 def place( self, nodename ):
713 """Simple placement algorithm:
714 place nodes into evenly sized bins"""
715 # Place nodes into bins
716 if nodename in self.hset:
717 server = self.servdict[ self.hind / self.hbin ]
719 elif nodename in self.sset:
720 server = self.servdict[ self.sind / self.sbin ]
722 elif nodename in self.cset:
723 server = self.servdict[ self.cind / self.cbin ]
726 info( 'warning: unknown node', nodename )
727 server = self.servdict[ 0 ]
731 # The MininetCluster class is not strictly necessary.
732 # However, it has several purposes:
733 # 1. To set up ssh connection sharing/multiplexing
734 # 2. To pre-flight the system so that everything is more likely to work
735 # 3. To allow connection/connectivity monitoring
736 # 4. To support pluggable placement algorithms
738 class MininetCluster( Mininet ):
740 "Cluster-enhanced version of Mininet class"
742 # Default ssh command
743 # BatchMode yes: don't ask for password
744 # ForwardAgent yes: forward authentication credentials
745 sshcmd = [ 'ssh', '-o', 'BatchMode=yes', '-o', 'ForwardAgent=yes' ]
747 def __init__( self, *args, **kwargs ):
748 """servers: a list of servers to use (note: include
749 localhost or None to use local system as well)
750 user: user name for server ssh
751 placement: Placer() subclass"""
752 params = { 'host': RemoteHost,
753 'switch': RemoteOVSSwitch,
754 'link': RemoteGRELink,
756 params.update( kwargs )
757 servers = params.pop( 'servers', [ 'localhost' ] )
758 servers = [ s if s else 'localhost' for s in servers ]
759 self.servers = servers
760 self.serverIP = params.pop( 'serverIP', {} )
761 if not self.serverIP:
762 self.serverIP = { server: RemoteMixin.findServerIP( server )
763 for server in self.servers }
764 self.user = params.pop( 'user', findUser() )
765 if params.pop( 'precheck' ):
767 self.connections = {}
768 self.placement = params.pop( 'placement', SwitchBinPlacer ) #Usa SwitchBinPlacer by default
769 # Make sure control directory exists
770 self.cdir = os.environ[ 'HOME' ] + '/.ssh/mn'
771 errRun( [ 'mkdir', '-p', self.cdir ] )
772 Mininet.__init__( self, *args, **params )
774 def popen( self, cmd ):
775 "Popen() for server connections"
776 assert self # please pylint
777 old = signal( SIGINT, SIG_IGN )
778 conn = Popen( cmd, stdin=PIPE, stdout=PIPE, close_fds=True )
779 signal( SIGINT, old )
782 def baddLink( self, *args, **kwargs ):
783 "break addlink for testing"
786 def precheck( self ):
787 """Pre-check to make sure connection works and that
788 we can call sudo without a password"""
790 info( '*** Checking servers\n' )
791 for server in self.servers:
792 ip = self.serverIP[ server ]
793 if not server or server == 'localhost':
796 dest = '%s@%s' % ( self.user, ip )
797 cmd = [ 'sudo', '-E', '-u', self.user ]
798 cmd += self.sshcmd + [ '-n', dest, 'sudo true' ]
799 debug( ' '.join( cmd ), '\n' )
800 _out, _err, code = errRun( cmd )
802 error( '\nstartConnection: server connection check failed '
803 'to %s using command:\n%s\n'
804 % ( server, ' '.join( cmd ) ) )
807 error( '*** Server precheck failed.\n'
808 '*** Make sure that the above ssh command works'
810 '*** You may also need to run mn -c on all nodes, and/or\n'
811 '*** use sudo -E.\n' )
815 def modifiedaddHost( self, *args, **kwargs ):
816 "Slightly modify addHost"
817 assert self # please pylint
818 kwargs[ 'splitInit' ] = True
819 return Mininet.addHost( *args, **kwargs )
821 def placeNodes( self ): #Prestarle atenciona este metodo...
822 """Place nodes on servers (if they don't have a server), and
823 start shell processes"""
824 if not self.servers or not self.topo:
825 # No shirt, no shoes, no service
827 nodes = self.topo.nodes()
828 placer = self.placement( servers=self.servers,
829 nodes=self.topo.nodes(),
830 hosts=self.topo.hosts(),
831 switches=self.topo.switches(),
832 links=self.topo.links() )
834 config = self.topo.nodeInfo( node )
835 # keep local server name consistent accross nodes
836 if 'server' in config.keys() and config[ 'server' ] is None:
837 config[ 'server' ] = 'localhost'
838 server = config.setdefault( 'server', placer.place( node ) )
840 config.setdefault( 'serverIP', self.serverIP[ server ] )
841 info( '%s:%s ' % ( node, server ) )
842 key = ( None, server )
843 _dest, cfile, _conn = self.connections.get(
844 key, ( None, None, None ) )
846 config.setdefault( 'controlPath', cfile )
848 def addController( self, *args, **kwargs ):
849 "Patch to update IP address to global IP address"
850 controller = Mininet.addController( self, *args, **kwargs )
851 loopback = '127.0.0.1'
852 if ( not isinstance( controller, Controller ) or
853 controller.IP() != loopback ):
855 # Find route to a different server IP address
856 serverIPs = [ ip for ip in self.serverIP.values()
857 if ip is not controller.IP() ]
859 return # no remote servers - loopback is fine
860 remoteIP = serverIPs[ 0 ]
861 # Route should contain 'dev <intfname>'
862 route = controller.cmd( 'ip route get', remoteIP,
863 r'| egrep -o "dev\s[^[:space:]]+"' )
865 raise Exception('addController: no route from', controller,
867 intf = route.split()[ 1 ].strip()
868 debug( 'adding', intf, 'to', controller )
869 Intf( intf, node=controller ).updateIP()
870 debug( controller, 'IP address updated to', controller.IP() )
873 def buildFromTopo( self, *args, **kwargs ):
875 info( '*** Placing nodes(estoy en cluster.py class mncluster)\n' )
878 Mininet.buildFromTopo( self, *args, **kwargs )
881 def testNsTunnels( remote='ubuntu2', link=RemoteGRELink ):
882 "Test tunnels between nodes in namespaces"
883 net = Mininet( host=RemoteHost, link=link )
884 h1 = net.addHost( 'h1')
885 h2 = net.addHost( 'h2', server=remote )
886 net.addLink( h1, h2 )
891 # Manual topology creation with net.add*()
893 # This shows how node options may be used to manage
894 # cluster placement using the net.add*() API
896 def testRemoteNet( remote='ubuntu2', link=RemoteGRELink ):
897 "Test remote Node classes"
898 info( '*** Remote Node Test\n' )
899 net = Mininet( host=RemoteHost, switch=RemoteOVSSwitch, link=link )
900 c0 = net.addController( 'c0' )
901 # Make sure controller knows its non-loopback address
902 Intf( 'eth0', node=c0 ).updateIP()
903 info( "*** Creating local h1\n" )
904 h1 = net.addHost( 'h1' )
905 info( "*** Creating remote h2\n" )
906 h2 = net.addHost( 'h2', server=remote )
907 info( "*** Creating local s1\n" )
908 s1 = net.addSwitch( 's1' )
909 info( "*** Creating remote s2\n" )
910 s2 = net.addSwitch( 's2', server=remote )
911 info( "*** Adding links\n" )
912 net.addLink( h1, s1 )
913 net.addLink( s1, s2 )
914 net.addLink( h2, s2 )
916 info( 'Mininet is running on', quietRun( 'hostname' ).strip(), '\n' )
917 for node in c0, h1, h2, s1, s2:
918 info( 'Node', node, 'is running on',
919 node.cmd( 'hostname' ).strip(), '\n' )
925 # High-level/Topo API example
927 # This shows how existing Mininet topologies may be used in cluster
928 # mode by creating node placement functions and a controller which
929 # can be accessed remotely. This implements a very compatible version
930 # of cluster edition with a minimum of code!
932 remoteHosts = [ 'h2' ]
933 remoteSwitches = [ 's2' ]
934 remoteServer = 'ubuntu2'
936 def HostPlacer( name, *args, **params ):
937 "Custom Host() constructor which places hosts on servers"
938 if name in remoteHosts:
939 return RemoteHost( name, *args, server=remoteServer, **params )
941 return Host( name, *args, **params )
943 def SwitchPlacer( name, *args, **params ):
944 "Custom Switch() constructor which places switches on servers"
945 if name in remoteSwitches:
946 return RemoteOVSSwitch( name, *args, server=remoteServer, **params )
948 return RemoteOVSSwitch( name, *args, **params )
950 def ClusterController( *args, **kwargs):
951 "Custom Controller() constructor which updates its eth0 IP address"
952 controller = Controller( *args, **kwargs )
953 # Find out its IP address so that cluster switches can connect
954 Intf( 'eth0', node=controller ).updateIP()
957 def testRemoteTopo( link=RemoteGRELink ):
958 "Test remote Node classes using Mininet()/Topo() API"
959 topo = LinearTopo( 2 )
960 net = Mininet( topo=topo, host=HostPlacer, switch=SwitchPlacer,
961 link=link, controller=ClusterController )
966 # Need to test backwards placement, where each host is on
967 # a server other than its switch!! But seriously we could just
968 # do random switch placement rather than completely random
971 def testRemoteSwitches( remote='ubuntu2', link=RemoteGRELink ):
972 "Test with local hosts and remote switches"
973 servers = [ 'localhost', remote]
974 topo = TreeTopo( depth=4, fanout=2 )
975 net = MininetCluster( topo=topo, servers=servers, link=link,
976 placement=RoundRobinPlacer )
982 # For testing and demo purposes it would be nice to draw the
983 # network graph and color it based on server.
985 # The MininetCluster() class integrates pluggable placement
986 # functions, for maximum ease of use. MininetCluster() also
987 # pre-flights and multiplexes server connections.
989 def testMininetCluster( remote='ubuntu2', link=RemoteGRELink ):
990 "Test MininetCluster()"
991 servers = [ 'localhost', remote ]
992 topo = TreeTopo( depth=3, fanout=3 )
993 net = MininetCluster( topo=topo, servers=servers, link=link,
994 placement=SwitchBinPlacer )
999 def signalTest( remote='ubuntu2'):
1000 "Make sure hosts are robust to signals"
1001 h = RemoteHost( 'h0', server=remote )
1002 h.shell.send_signal( SIGINT )
1004 if h.shell.returncode is None:
1005 info( 'signalTest: SUCCESS: ', h, 'has not exited after SIGINT', '\n' )
1007 info( 'signalTest: FAILURE:', h, 'exited with code',
1008 h.shell.returncode, '\n' )
1012 if __name__ == '__main__':
1013 setLogLevel( 'info' )
1014 remoteServer = 'ubuntu2'
1015 remoteLink = RemoteGRELink
1016 testRemoteTopo(link=remoteLink)
1017 testNsTunnels( remote=remoteServer, link=remoteLink )
1018 testRemoteNet( remote=remoteServer, link=remoteLink)
1019 testMininetCluster( remote=remoteServer, link=remoteLink)
1020 testRemoteSwitches( remote=remoteServer, link=remoteLink)
1021 signalTest( remote=remoteServer )