1d9d76298fe5e260ed680f88d925cc5fbc9d30e9
[vsorcdistro/.git] / mininet / examples / cluster.py
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
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
87
88 from signal import signal, SIGINT, SIG_IGN
89 from subprocess import Popen, PIPE, STDOUT
90 import os
91 from random import randrange
92 import sys
93 import re
94 from itertools import groupby
95 from operator import attrgetter
96 from distutils.version import StrictVersion
97
98
99 def findUser():
100     "Try to return logged-in (usually non-root) user"
101     return (
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() )
108
109
110 class ClusterCleanup( object ):
111     "Cleanup callback"
112
113     inited = False
114     serveruser = {}
115
116     @classmethod
117     def add( cls, server, user='' ):
118         "Add an entry to server: user dict"
119         if not cls.inited:
120             addCleanupCallback( cls.cleanup )
121         if not user:
122             user = findUser()
123         cls.serveruser[ server ] = user
124
125     @classmethod
126     def cleanup( cls ):
127         "Clean up"
128         info( '***Limpiando todo...\n' )
129         for server, user in cls.serveruser.items():
130             if server == 'localhost':
131                 # Handled by mininet.clean.cleanup()
132                 continue
133             else:
134                 cmd = [ 'su', user, '-c',
135                         'ssh %s@%s sudo mn -c' % ( user, server ) ]
136                 info( cmd, '\n' )
137                 info( quietRun( cmd ) )
138
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.
146
147 class RemoteMixin( object ):
148
149     "A mix-in class to turn local nodes into remote nodes"
150
151     # ssh base command
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' ]
158
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
182             if self.controlPath:
183                 self.sshcmd += [ '-o', 'ControlPath=' + self.controlPath,
184                                  '-o', 'ControlMaster=auto',
185                                  '-o', 'ControlPersist=' + '1' ]
186             self.sshcmd += [ self.dest ]
187             self.isRemote = True
188         else:
189             self.dest = None
190             self.sshcmd = []
191             self.isRemote = False
192         # Satisfy pylint
193         self.shell, self.pid = None, None
194         super( RemoteMixin, self ).__init__( name, **kwargs )
195
196     # Determine IP address of local host
197     _ipMatchRegex = re.compile( r'\d+\.\d+\.\d+\.\d+' )
198
199     @classmethod
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 )
204         if ipmatch:
205             return ipmatch[ 0 ]
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
210         return ip
211
212     # Command support via shell process in namespace
213     def startShell( self, *args, **kwargs ):
214         "Start a shell process for running commands"
215         if self.isRemote:
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:
221             self.finishInit()
222
223     def finishInit( self ):
224         "Wait for split initialization to complete"
225         self.pid = int( self.waitOutput() )
226
227     def rpopen( self, *cmd, **opts ):
228         "Return a Popen object on underlying server in root namespace"
229         params = { 'stdin': PIPE,
230                    'stdout': PIPE,
231                    'stderr': STDOUT,
232                    'sudo': True }
233         params.update( opts )
234         return self._popen( *cmd, **params )
235
236     def rcmd( self, *cmd, **opts):
237         """rcmd: run a command on underlying server
238            in root namespace
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.
246         result = ''
247         while True:
248             poll = popen.poll()
249             result += popen.stdout.read()
250             if poll is not None:
251                 break
252         return result
253
254     @staticmethod
255     def _ignoreSignal():
256         "Detach from process group to ignore all signals"
257         os.setpgrp()
258
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:
265             cmd = cmd.split()
266         if self.isRemote:
267             if sudo:
268                 cmd = [ 'sudo', '-E' ] + cmd
269             if tt:
270                 cmd = self.sshcmd + cmd
271             else:
272                 # Hack: remove -tt
273                 sshcmd = list( self.sshcmd )
274                 sshcmd.remove( '-tt' )
275                 cmd = sshcmd + cmd
276         else:
277             if self.user and not sudo:
278                 # Drop privileges
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 )
283         return popen
284
285     def popen( self, *args, **kwargs ):
286         "Override: disable -tt"
287         return super( RemoteMixin, self).popen( *args, tt=False, **kwargs )
288
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 )
293
294
295 class RemoteNode( RemoteMixin, Node ):
296     "A node on a remote server"
297     pass
298
299
300 class RemoteHost( RemoteNode ):
301     "A RemoteHost is simply a RemoteNode"
302     pass
303
304
305 class RemoteOVSSwitch( RemoteMixin, OVSSwitch ):
306     "Remote instance of Open vSwitch"
307
308     OVSVersions = {}
309
310     def __init__( self, *args, **kwargs ):
311         # No batch startup yet
312         kwargs.update( batch=True )
313         super( RemoteOVSSwitch, self ).__init__( *args, **kwargs )
314
315     def isOldOVS( self ):
316         "Is remote switch using an old OVS version?"
317         cls = type( self )
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' ) )
326
327     @classmethod
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 )
334             switch = group[ 0 ]
335             OVSSwitch.batchStartup( group, run=switch.cmd )
336         return switches
337
338     @classmethod
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 )
345             switch = group[ 0 ]
346             OVSSwitch.batchShutdown( group, run=switch.rcmd )
347         return switches
348
349
350 class RemoteLink( Link ):
351     "A RemoteLink is a link between nodes which may be on different servers (SSH Link)"
352
353     def __init__( self, node1, node2, **kwargs ):
354         """Initialize a RemoteLink
355            see Link() for parameters"""
356         # Create links on remote node
357         self.node1 = node1
358         self.node2 = node2
359         self.tunnel = None
360         kwargs.setdefault( 'params1', {} )
361         kwargs.setdefault( 'params2', {} )
362         self.cmd = None  # satisfy pylint
363         Link.__init__( self, node1, node2, **kwargs )
364
365     def stop( self ):
366         "Stop this link"
367         if self.tunnel:
368             self.tunnel.terminate()
369             self.intf1.delete()
370             self.intf2.delete()
371         else:
372             Link.stop( self )
373         self.tunnel = None
374
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,
392                                        addr1, addr2 )
393         return self.tunnel
394
395     @staticmethod
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"""
401         intf = str( intf )
402         cmd = 'ip link set %s netns %s' % ( intf, node.pid )
403         result = node.rcmd( cmd )
404         if result:
405             raise Exception('error executing command %s' % cmd)
406         return True
407
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,
417                                     addr2, addr1 )
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 )
425             if result:
426                 raise Exception( 'error creating tap9 on %s: %s' %
427                                  ( node, result ) )
428         # 2. Create ssh tunnel between tap interfaces
429         # -n: close stdin
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',
433                 dest, 'echo @' ]
434         self.cmd = cmd
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 )
440         if ch != '@':
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 ) ):
453             if not addr:
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
457             else:
458                 node.cmd( 'sudo ip link set tap9 down' ) #personalizada
459                 result = node.cmd( 'sudo ip link set tap9 name', intf,
460                                     'address', addr )
461                 node.cmd(  'sudo ip link set', intf, 'up') #personalizada
462             if result:
463                 raise Exception( 'error renaming %s: %s' % ( intf, result ) )
464         return tunnel
465
466     def status( self ):
467         "Detailed representation of link"
468         if self.tunnel:
469             if self.tunnel.poll() is not None:
470                 status = "Tunnel EXITED %s" % self.tunnel.returncode
471             else:
472                 status = "Tunnel Running (%s: %s)" % (
473                     self.tunnel.pid, self.cmd )
474         else:
475             status = "OK"
476         result = "%s %s" % ( Link.status( self ), status )
477         return result
478
479
480 class RemoteSSHLink( RemoteLink ):
481     "Remote link using SSH tunnels"
482     def __init__(self, node1, node2, **kwargs):
483         RemoteLink.__init__( self, node1, node2, **kwargs )
484
485
486 class RemoteGRELink( RemoteLink ):
487     "Remote link using GRE tunnels"
488
489     GRE_KEY = 0
490
491     def __init__(self, node1, node2, **kwargs):
492         RemoteLink.__init__( self, node1, node2, **kwargs )
493
494     def stop( self ):
495         "Stop this link"
496         if self.tunnel:
497             self.intf1.delete()
498             self.intf2.delete()
499         else:
500             Link.stop( self )
501         self.tunnel = None
502
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)
523         else:
524             # Otherwise, make a tunnel
525             self.makeTunnel( node1, node2, intfname1, intfname2, addr1, addr2 )
526             self.tunnel = 1
527
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,
535                                     addr2, addr1 )
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) )
553             if result:
554                 raise Exception('error creating gretap on %s: %s'
555                                 % (node, result))
556             if addr:
557                 node.rcmd('ip link set %s address %s' % (intfname, addr))
558
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)
563
564
565 # Some simple placement algorithms for MininetCluster
566
567 class Placer( object ):
568     "Node placement algorithm for MininetCluster"
569
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
575            hosts: list of hosts
576            switches: list of switches
577            controllers: list of controllers
578            links: list of links
579            (all arguments are optional)
580            returns: server"""
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 []
587
588     def place( self, node ):
589         "Return server for a given node"
590         assert self, node  # satisfy pylint
591         # Default placement: run locally
592         return 'localhost'
593
594
595 class RandomPlacer( Placer ):
596     "Random placement"
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 ) ) ]
603
604
605 class RoundRobinPlacer( Placer ):
606     """Round-robin placement
607        Note this will usually result in cross-server links between
608        hosts and switches"""
609
610     def __init__( self, *args, **kwargs ):
611         Placer.__init__( self, *args, **kwargs )
612         self.next = 0
613
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 )
621         return server
622
623
624 class SwitchBinPlacer( Placer ):
625     """Place switches (and controllers) into evenly-sized bins,
626        and attempt to co-locate hosts and switches"""
627
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()
637
638     @staticmethod
639     def bin( nodes, servers ):
640         "Distribute nodes evenly over servers"
641         # Calculate base bin size
642         nlen = len( nodes )
643         slen = len( servers )
644         # Basic bin size
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 ) }
656
657     def calculatePlacement( self ):
658         "Pre-calculate node placement"
659         placement = {}
660         # Create host-switch connectivity map,
661         # associating host with last switch that it's
662         # connected to
663         switchFor = {}
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
669         # Place switches
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
674         for h in self.hosts:
675             if h in placement:
676                 # Host is already placed - leave it there
677                 continue
678             if h in switchFor:
679                 placement[ h ] = placement[ switchFor[ h ] ]
680             else:
681                 raise Exception(
682                         "SwitchBinPlacer: cannot place isolated host " + h )
683         return placement
684
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 ]
690
691
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"""
696
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
711
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 ]
718             self.hind += 1
719         elif nodename in self.sset:
720             server = self.servdict[ self.sind / self.sbin ]
721             self.sind += 1
722         elif nodename in self.cset:
723             server = self.servdict[ self.cind / self.cbin ]
724             self.cind += 1
725         else:
726             info( 'warning: unknown node', nodename )
727             server = self.servdict[ 0 ]
728         return server
729
730
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
737
738 class MininetCluster( Mininet ):
739
740     "Cluster-enhanced version of Mininet class"
741
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' ]
746
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,
755                    'precheck': True }
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' ):
766             self.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 )
773
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 )
780         return conn
781
782     def baddLink( self, *args, **kwargs ):
783         "break addlink for testing"
784         pass
785
786     def precheck( self ):
787         """Pre-check to make sure connection works and that
788            we can call sudo without a password"""
789         result = 0
790         info( '*** Checking servers\n' )
791         for server in self.servers:
792             ip = self.serverIP[ server ]
793             if not server or server == 'localhost':
794                 continue
795             info( server, '' )
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 )
801             if code != 0:
802                 error( '\nstartConnection: server connection check failed '
803                        'to %s using command:\n%s\n'
804                         % ( server, ' '.join( cmd ) ) )
805             result |= code
806         if result:
807             error( '*** Server precheck failed.\n'
808                    '*** Make sure that the above ssh command works'
809                    ' correctly.\n'
810                    '*** You may also need to run mn -c on all nodes, and/or\n'
811                    '*** use sudo -E.\n' )
812             sys.exit( 1 )
813         info( '\n' )
814
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 )
820
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
826             return
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() )
833         for node in nodes:
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 ) )
839             if server:
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 ) )
845             if cfile:
846                 config.setdefault( 'controlPath', cfile )
847
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 ):
854             return
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() ]
858         if not serverIPs:
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:]]+"' )
864         if not route:
865             raise Exception('addController: no route from', controller,
866                             'to', remoteIP )
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() )
871         return controller
872
873     def buildFromTopo( self, *args, **kwargs ):
874         "Start network"
875         info( '*** Placing nodes(estoy en cluster.py class mncluster)\n' )
876         self.placeNodes()
877         info( '\n' )
878         Mininet.buildFromTopo( self, *args, **kwargs )
879
880
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 )
887     net.start()
888     net.pingAll()
889     net.stop()
890
891 # Manual topology creation with net.add*()
892 #
893 # This shows how node options may be used to manage
894 # cluster placement using the net.add*() API
895
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 )
915     net.start()
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' )
920     net.pingAll()
921     CLI( net )
922     net.stop()
923
924
925 # High-level/Topo API example
926 #
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!
931
932 remoteHosts = [ 'h2' ]
933 remoteSwitches = [ 's2' ]
934 remoteServer = 'ubuntu2'
935
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 )
940     else:
941         return Host( name, *args, **params )
942
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 )
947     else:
948         return RemoteOVSSwitch( name, *args, **params )
949
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()
955     return controller
956
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 )
962     net.start()
963     net.pingAll()
964     net.stop()
965
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
969 # host placement.
970
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 )
977     net.start()
978     net.pingAll()
979     net.stop()
980
981
982 # For testing and demo purposes it would be nice to draw the
983 # network graph and color it based on server.
984
985 # The MininetCluster() class integrates pluggable placement
986 # functions, for maximum ease of use. MininetCluster() also
987 # pre-flights and multiplexes server connections.
988
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 )
995     net.start()
996     net.pingAll()
997     net.stop()
998
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 )
1003     h.shell.poll()
1004     if h.shell.returncode is None:
1005         info( 'signalTest: SUCCESS: ', h, 'has not exited after SIGINT', '\n' )
1006     else:
1007         info( 'signalTest: FAILURE:', h, 'exited with code',
1008               h.shell.returncode, '\n' )
1009     h.stop()
1010
1011
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 )