diff --git a/bin/mn b/bin/mn index f4b16eff4cd947b6db628987de98632fc86233eb..f5643445959e237db528885369990bea0d6f019e 100755 --- a/bin/mn +++ b/bin/mn @@ -22,7 +22,7 @@ if 'PYTHONPATH' in os.environ: from mininet.clean import cleanup from mininet.cli import CLI -from mininet.log import lg, LEVELS, info, debug, error +from mininet.log import lg, LEVELS, info, debug, warn, error from mininet.net import Mininet, MininetWithControlNet, VERSION from mininet.node import ( Host, CPULimitedHost, Controller, OVSController, NOX, RemoteController, DefaultController, @@ -35,6 +35,15 @@ from mininet.topolib import TreeTopo, TorusTopo from mininet.util import custom, customConstructor from mininet.util import buildTopo +from functools import partial + +# Experimental! cluster edition prototype +from mininet.examples.cluster import ( MininetCluster, RemoteHost, + RemoteOVSSwitch, RemoteLink, + SwitchBinPlacer, RandomPlacer ) +from mininet.examples.clustercli import DemoCLI as ClusterCLI + +PLACEMENT = { 'block': SwitchBinPlacer, 'random': RandomPlacer } # built in topologies, created only when run TOPODEF = 'minimal' @@ -204,6 +213,14 @@ class MininetRunner( object ): default=False, help="adds a NAT to the topology " "that connects Mininet to the physical network" ) opts.add_option( '--version', action='callback', callback=version ) + opts.add_option( '--cluster', type='string', default=None, + metavar='server1,server2...', + help=( 'run on multiple servers (experimental!)' ) ) + opts.add_option( '--placement', type='choice', + choices=PLACEMENT.keys(), default='block', + metavar='block|random', + help=( 'node placement for --cluster ' + '(experimental!) ' ) ) self.options, self.args = opts.parse_args() @@ -226,6 +243,8 @@ class MininetRunner( object ): def begin( self ): "Create and run mininet." + global CLI + if self.options.clean: cleanup() exit() @@ -241,8 +260,6 @@ class MininetRunner( object ): if self.validate: self.validate( self.options ) - inNamespace = self.options.innamespace - Net = MininetWithControlNet if inNamespace else Mininet ipBase = self.options.ipbase xterms = self.options.xterms mac = self.options.mac @@ -251,6 +268,22 @@ class MininetRunner( object ): listenPort = None if not self.options.nolistenport: listenPort = self.options.listenport + + # Handle inNamespace, cluster options + inNamespace = self.options.innamespace + cluster = self.options.cluster + if inNamespace and cluster: + print "Please specify --innamespace OR --cluster" + exit() + Net = MininetWithControlNet if inNamespace else Mininet + if cluster: + warn( '*** WARNING: Experimental cluster mode!\n' + '*** Using RemoteHost, RemoteOVSSwitch, RemoteLink\n' ) + host, switch, link = RemoteHost, RemoteOVSSwitch, RemoteLink + CLI = ClusterCLI + Net = partial( MininetCluster, servers=cluster.split( ',' ), + placement=PLACEMENT[ self.options.placement ] ) + mn = Net( topo=topo, switch=switch, host=host, controller=controller, link=link, diff --git a/examples/cluster.py b/examples/cluster.py new file mode 100755 index 0000000000000000000000000000000000000000..bd00a6d2f4ca2174b3e34a678a49f81a69a7d18b --- /dev/null +++ b/examples/cluster.py @@ -0,0 +1,831 @@ +#!/usr/bin/python + +""" +cluster.py: prototyping/experimentation for distributed Mininet, + aka Mininet: Cluster Edition + +Author: Bob Lantz + +Core classes: + + RemoteNode: a Node() running on a remote server + RemoteOVSSwitch(): an OVSSwitch() running on a remote server + RemoteLink: a Link() on a remote server + Tunnel: a Link() between a local Node() and a RemoteNode() + +These are largely interoperable with local objects. + +- One Mininet to rule them all + +It is important that the same topologies, APIs, and CLI can be used +with minimal or no modification in both local and distributed environments. + +- Multiple placement models + +Placement should be as easy as possible. We should provide basic placement +support and also allow for explicit placement. + +Questions: + +What is the basic communication mechanism? + +To start with? Probably a single multiplexed ssh connection between each +pair of mininet servers that needs to communicate. + +How are tunnels created? + +We have several options including ssh, GRE, OF capsulator, socat, VDE, l2tp, +etc.. It's not clear what the best one is. For now, we use ssh tunnels since +they are encrypted and semi-automatically shared. We will probably want to +support GRE as well because it's very easy to set up with OVS. + +How are tunnels destroyed? + +They are destroyed when the links are deleted in Mininet.stop() + +How does RemoteNode.popen() work? + +It opens a shared ssh connection to the remote server and attaches to +the namespace using mnexec -a -g. + +Is there any value to using Paramiko vs. raw ssh? + +Maybe, but it doesn't seem to support L2 tunneling. + +Should we preflight the entire network, including all server-to-server +connections? + +Yes! We don't yet do this with remote server-to-server connections yet. + +Should we multiplex the link ssh connections? + +Yes, this is done automatically with ControlMaster=auto. + +Note on ssh and DNS: +Please add UseDNS: no to your /etc/ssh/sshd_config!!! + +Things to do: + +- asynchronous/pipelined/parallel startup +- ssh debugging/profiling +- make connections into real objects +- support for other tunneling schemes +- tests and benchmarks +- hifi support (e.g. delay compensation) +""" + +from mininet.node import Node, Host, OVSSwitch, Controller +from mininet.link import Link, Intf +from mininet.net import Mininet +from mininet.topo import LinearTopo +from mininet.topolib import TreeTopo +from mininet.util import quietRun, makeIntfPair, errRun, retry +from mininet.examples.clustercli import CLI +from mininet.log import setLogLevel, debug, info, error + +from signal import signal, SIGINT, SIGHUP, SIG_IGN +from subprocess import Popen, PIPE, STDOUT +import os +from random import randrange +from sys import exit +import re + +from distutils.version import StrictVersion + +# BL note: so little code is required for remote nodes, +# we will probably just want to update the main Node() +# class to enable it for remote access! However, there +# are a large number of potential failure conditions with +# remote nodes which we may want to detect and handle. +# Another interesting point is that we could put everything +# in a mix-in class and easily add cluster mode to 2.0. + +class RemoteMixin( object ): + + "A mix-in class to turn local nodes into remote nodes" + + # ssh base command + # -q: don't print stupid diagnostic messages + # BatchMode yes: don't ask for password + # ForwardAgent yes: forward authentication credentials + sshbase = [ 'ssh', '-q', + '-o', 'BatchMode=yes', + '-o', 'ForwardAgent=yes', '-tt' ] + + def __init__( self, name, server=None, user=None, serverIP=None, + controlPath='/tmp/mn-%r@%h:%p', splitInit=False, **kwargs): + """Instantiate a remote node + name: name of remote node + server: remote server (optional) + user: user on remote server (optional) + controlPath: ssh control path template (optional) + splitInit: split initialization? + **kwargs: see Node()""" + # We connect to servers by IP address + if server == 'localhost': + server = None + self.server = server + if not serverIP: + serverIP = self.findServerIP( server ) + self.serverIP = serverIP + if not user: + user = quietRun( 'who am i' ).split()[ 0 ] + self.user = user + if self.user and self.server: + self.dest = '%s@%s' % ( self.user, self.serverIP ) + else: + self.dest = None + self.controlPath = controlPath + self.sshcmd = [] + if self.dest: + self.sshcmd = [ 'sudo', '-E', '-u', self.user ] + self.sshbase + if self.controlPath: + self.sshcmd += [ '-o', 'ControlPath=' + self.controlPath, + '-o', 'ControlMaster=auto' ] + self.sshcmd = self.sshcmd + [ self.dest ] + self.splitInit = splitInit + super( RemoteMixin, self ).__init__( name, **kwargs ) + + # Determine IP address of local host + _ipMatchRegex = re.compile( r'\d+\.\d+\.\d+\.\d+' ) + + @classmethod + def findServerIP( cls, server, intf='eth0' ): + "Return our server's IP address" + # Check for this server + if not server: + output = quietRun( 'ifconfig %s' % intf ) + # Otherwise, handle remote server + else: + # First, check for an IP address + if server: + ipmatch = cls._ipMatchRegex.findall( server ) + if ipmatch: + return ipmatch[ 0 ] + # Otherwise, look up remote server + output = quietRun( 'getent ahostsv4 %s' % server ) + ips = cls._ipMatchRegex.findall( output ) + ip = ips[ 0 ] if ips else None + return ip + + # Command support via shell process in namespace + def startShell( self, *args, **kwargs ): + "Start a shell process for running commands" + if self.dest: + kwargs.update( mnopts='-c' ) + super( RemoteMixin, self ).startShell( *args, **kwargs ) + if self.splitInit: + self.sendCmd( 'echo $$' ) + else: + self.pid = int( self.cmd( 'echo $$' ) ) + + def finishInit( self ): + self.pid = int( self.waitOutput() ) + + def rpopen( self, *cmd, **opts ): + "Return a Popen object on underlying server in root namespace" + params = { 'stdin': PIPE, + 'stdout': PIPE, + 'stderr': STDOUT, + 'sudo': True } + params.update( opts ) + return self._popen( *cmd, **params ) + + def rcmd( self, *cmd, **opts): + """rcmd: run a command on underlying server + in root namespace + args: string or list of strings + returns: stdout and stderr""" + popen = self.rpopen( *cmd, **opts ) + # print 'RCMD: POPEN:', popen + # These loops are tricky to get right. + # Once the process exits, we can read + # EOF twice if necessary. + result = '' + while True: + poll = popen.poll() + result += popen.stdout.read() + if poll is not None: + break + return result + + @staticmethod + def _ignoreSignal(): + "Detach from process group to ignore all signals" + os.setpgrp() + + def _popen( self, cmd, sudo=True, tt=True, **params): + """Spawn a process on a remote node + cmd: remote command to run (list) + **params: parameters to Popen() + returns: Popen() object""" + if type( cmd ) is str: + cmd = cmd.split() + if self.dest: + if sudo: + cmd = [ 'sudo', '-E' ] + cmd + if tt: + cmd = self.sshcmd + cmd + else: + # Hack: remove -tt + sshcmd = list( self.sshcmd ) + sshcmd.remove( '-tt' ) + cmd = sshcmd + cmd + else: + if self.user and not sudo: + # Drop privileges + cmd = [ 'sudo', '-E', '-u', self.user ] + cmd + params.update( preexec_fn=self._ignoreSignal ) + debug( '_popen', ' '.join(cmd), params ) + popen = super( RemoteMixin, self )._popen( cmd, **params ) + return popen + + def popen( self, *args, **kwargs ): + "Override: disable -tt" + return super( RemoteMixin, self).popen( *args, tt=False, **kwargs ) + + def addIntf( self, *args, **kwargs ): + "Override: use RemoteLink.moveIntf" + return super( RemoteMixin, self).addIntf( *args, + moveIntfFn=RemoteLink.moveIntf, **kwargs ) + + +class RemoteNode( RemoteMixin, Node ): + "A node on a remote server" + pass + + +class RemoteHost( RemoteNode ): + "A RemoteHost is simply a RemoteNode" + pass + + +class RemoteOVSSwitch( RemoteMixin, OVSSwitch ): + "Remote instance of Open vSwitch" + OVSVersions = {} + def isOldOVS( self ): + "Is remote switch using an old OVS version?" + cls = type( self ) + if self.server not in cls.OVSVersions: + vers = self.cmd( 'ovs-vsctl --version' ) + cls.OVSVersions[ self.server ] = re.findall( '\d+\.\d+', vers )[ 0 ] + return ( StrictVersion( cls.OVSVersions[ self.server ] ) < + StrictVersion( '1.10' ) ) + + + +class RemoteLink( Link ): + + "A RemoteLink is a link between nodes which may be on different servers" + + def __init__( self, node1, node2, **kwargs ): + """Initialize a RemoteLink + see Link() for parameters""" + # Create links on remote node + self.node1 = node1 + self.node2 = node2 + self.tunnel = None + kwargs.setdefault( 'params1', {} ) + kwargs.setdefault( 'params2', {} ) + Link.__init__( self, node1, node2, **kwargs ) + + def stop( self ): + "Stop this link" + if self.tunnel: + self.tunnel.terminate() + self.tunnel = None + + def makeIntfPair( self, intfname1, intfname2, addr1=None, addr2=None ): + """Create pair of interfaces + intfname1: name of interface 1 + intfname2: name of interface 2 + (override this method [and possibly delete()] + to change link type)""" + node1, node2 = self.node1, self.node2 + server1 = getattr( node1, 'server', None ) + server2 = getattr( node2, 'server', None ) + if not server1 and not server2: + # Local link + return makeIntfPair( intfname1, intfname2, addr1, addr2 ) + elif server1 == server2: + # Remote link on same remote server + return makeIntfPair( intfname1, intfname2, addr1, addr2, + run=node1.rcmd ) + # Otherwise, make a tunnel + self.tunnel = self.makeTunnel( node1, node2, intfname1, intfname2, addr1, addr2 ) + return self.tunnel + + @staticmethod + def moveIntf( intf, node, printError=True ): + """Move remote interface from root ns to node + intf: string, interface + dstNode: destination Node + srcNode: source Node or None (default) for root ns + printError: if true, print error""" + intf = str( intf ) + cmd = 'ip link set %s netns %s' % ( intf, node.pid ) + node.rcmd( cmd ) + links = node.cmd( 'ip link show' ) + if not ( ' %s:' % intf ) in links: + if printError: + error( '*** Error: RemoteLink.moveIntf: ' + intf + + ' not successfully moved to ' + node.name + '\n' ) + return False + return True + + def makeTunnel( self, node1, node2, intfname1, intfname2, + addr1=None, addr2=None ): + "Make a tunnel across switches on different servers" + # 1. Create tap interfaces + for node in node1, node2: + # For now we are hard-wiring tap9, which we will rename + node.rcmd( 'ip link delete tap9', stderr=PIPE ) + cmd = 'ip tuntap add dev tap9 mode tap user ' + node.user + node.rcmd( cmd ) + links = node.rcmd( 'ip link show' ) + # print 'after add, links =', links + assert 'tap9' in links + # 2. Create ssh tunnel between tap interfaces + # -n: close stdin + dest = '%s@%s' % ( node2.user, node2.serverIP ) + cmd = [ 'ssh', '-n', '-o', 'Tunnel=Ethernet', '-w', '9:9', + dest, 'echo @' ] + self.cmd = cmd + tunnel = node1.rpopen( cmd, sudo=False ) + # When we receive the character '@', it means that our + # tunnel should be set up + debug( 'Waiting for tunnel to come up...\n' ) + ch = tunnel.stdout.read( 1 ) + if ch != '@': + error( 'makeTunnel:\n', + 'Tunnel setup failed for', + '%s:%s' % ( node1, node1.dest ), 'to', + '%s:%s\n' % ( node2, node2.dest ), + 'command was:', cmd, '\n' ) + tunnel.terminate() + tunnel.wait() + error( ch + tunnel.stdout.read() ) + error( tunnel.stderr.read() ) + exit( 1 ) + # 3. Move interfaces if necessary + for node in node1, node2: + if node.inNamespace: + retry( 3, .01, RemoteLink.moveIntf, 'tap9', node ) + # 4. Rename tap interfaces to desired names + for node, intf, addr in ( ( node1, intfname1, addr1 ), + ( node2, intfname2, addr2 ) ): + if not addr: + node.cmd( 'ip link set tap9 name', intf ) + else: + node.cmd( 'ip link set tap9 name', intf, 'address', addr ) + for node, intf in ( ( node1, intfname1 ), ( node2, intfname2 ) ): + assert intf in node.cmd( 'ip link show' ) + return tunnel + + def status( self ): + "Detailed representation of link" + if self.tunnel: + if self.tunnel.poll() is not None: + status = "Tunnel EXITED %s" % self.tunnel.returncode + else: + status = "Tunnel Running (%s: %s)" % ( + self.tunnel.pid, self.cmd ) + else: + status = "OK" + result = "%s %s" % ( Link.status( self ), status ) + return result + + +# Some simple placement algorithms for MininetCluster + +class Placer( object ): + "Node placement algorithm for MininetCluster" + + def __init__( self, servers=None, nodes=None, hosts=None, + switches=None, controllers=None, links=None ): + """Initialize placement object + servers: list of servers + nodes: list of all nodes + hosts: list of hosts + switches: list of switches + controllers: list of controllers + links: list of links + (all arguments are optional) + returns: server""" + self.servers = servers or [] + self.nodes = nodes or [] + self.hosts = hosts or [] + self.switches = switches or [] + self.controllers = controllers or [] + self.links = links or [] + + def place( self, node ): + "Return server for a given node" + # Default placement: run locally + return None + + +class RandomPlacer( Placer ): + "Random placement" + def place( self, nodename ): + """Random placement function + nodename: node name""" + # This may be slow with lots of servers + return self.servers[ randrange( 0, len( self.servers ) ) ] + + +class RoundRobinPlacer( Placer ): + """Round-robin placement + Note this will usually result in cross-server links between + hosts and switches""" + + def __init__( self, *args, **kwargs ): + Placer.__init__( self, *args, **kwargs ) + self.next = 0 + + def place( self, nodename ): + """Round-robin placement function + nodename: node name""" + # This may be slow with lots of servers + server = self.servers[ self.next ] + self.next = ( self.next + 1 ) % len( self.servers ) + return server + + +class SwitchBinPlacer( Placer ): + """Place switches (and controllers) into evenly-sized bins, + and attempt to co-locate hosts and switches""" + + def __init__( self, *args, **kwargs ): + Placer.__init__( self, *args, **kwargs ) + # Easy lookup for servers and node sets + self.servdict = dict( enumerate( self.servers ) ) + self.hset = frozenset( self.hosts ) + self.sset = frozenset( self.switches ) + self.cset = frozenset( self.controllers ) + # Server and switch placement indices + self.placement = self.calculatePlacement() + + @staticmethod + def bin( nodes, servers ): + "Distribute nodes evenly over servers" + # Calculate base bin size + nlen = len( nodes ) + slen = len( servers ) + # Basic bin size + quotient = int( nlen / slen ) + binsizes = { server: quotient for server in servers } + # Distribute remainder + remainder = nlen % slen + for server in servers[ 0 : remainder ]: + binsizes[ server ] += 1 + # Create binsize[ server ] tickets for each server + tickets = sum( [ binsizes[ server ] * [ server ] + for server in servers ], [] ) + # And assign one ticket to each node + return { node: ticket for node, ticket in zip( nodes, tickets ) } + + def calculatePlacement( self ): + "Pre-calculate node placement" + placement = {} + # Create host-switch connectivity map, + # associating host with last switch that it's + # connected to + switchFor = {} + for src, dst in self.links: + if src in self.hset and dst in self.sset: + switchFor[ src ] = dst + if dst in self.hset and src in self.sset: + switchFor[ dst ] = src + # Place switches + placement = self.bin( self.switches, self.servers ) + # Place controllers and merge into placement dict + placement.update( self.bin( self.controllers, self.servers ) ) + # Co-locate hosts with their switches + for h in self.hosts: + if h in placement: + # Host is already placed - leave it there + continue + if h in switchFor: + placement[ h ] = placement[ switchFor[ h ] ] + else: + raise Exception( + "SwitchBinPlacer: cannot place isolated host " + h ) + return placement + + def place( self, node ): + """Simple placement algorithm: + place switches into evenly sized bins, + and place hosts near their switches""" + return self.placement[ node ] + + +class HostSwitchBinPlacer( Placer ): + """Place switches *and hosts* into evenly-sized bins + Note that this will usually result in cross-server + links between hosts and switches""" + + def __init__( self, *args, **kwargs ): + Placer.__init__( self, *args, **kwargs ) + # Calculate bin sizes + scount = len( self.servers ) + self.hbin = max( int( len( self.hosts ) / scount ), 1 ) + self.sbin = max( int( len( self.switches ) / scount ), 1 ) + self.cbin = max( int( len( self.controllers ) / scount ) , 1 ) + info( 'scount:', scount ) + info( 'bins:', self.hbin, self.sbin, self.cbin, '\n' ) + self.servdict = dict( enumerate( self.servers ) ) + self.hset = frozenset( self.hosts ) + self.sset = frozenset( self.switches ) + self.cset = frozenset( self.controllers ) + self.hind, self.sind, self.cind = 0, 0, 0 + + def place( self, nodename ): + """Simple placement algorithm: + place nodes into evenly sized bins""" + # Place nodes into bins + if nodename in self.hset: + server = self.servdict[ self.hind / self.hbin ] + self.hind += 1 + elif nodename in self.sset: + server = self.servdict[ self.sind / self.sbin ] + self.sind += 1 + elif nodename in self.cset: + server = self.servdict[ self.cind / self.cbin ] + self.cind += 1 + else: + info( 'warning: unknown node', nodename ) + server = self.servdict[ 0 ] + return server + + + +# The MininetCluster class is not strictly necessary. +# However, it has several purposes: +# 1. To set up ssh connection sharing/multiplexing +# 2. To pre-flight the system so that everything is more likely to work +# 3. To allow connection/connectivity monitoring +# 4. To support pluggable placement algorithms + +class MininetCluster( Mininet ): + + "Cluster-enhanced version of Mininet class" + + # Default ssh command + # BatchMode yes: don't ask for password + # ForwardAgent yes: forward authentication credentials + sshcmd = [ 'ssh', '-o', 'BatchMode=yes', '-o', 'ForwardAgent=yes' ] + + def __init__( self, *args, **kwargs ): + """servers: a list of servers to use (note: include + localhost or None to use local system as well) + user: user name for server ssh + placement: Placer() subclass""" + params = { 'host': RemoteHost, + 'switch': RemoteOVSSwitch, + 'link': RemoteLink, + 'precheck': True } + params.update( kwargs ) + servers = params.pop( 'servers', [ None ] ) + servers = [ s if s != 'localhost' else None for s in servers ] + self.servers = servers + self.serverIP = params.pop( 'serverIP', {} ) + if not self.serverIP: + self.serverIP = { server: RemoteMixin.findServerIP( server ) + for server in self.servers } + self.user = params.pop( 'user', None ) + if self.servers and not self.user: + self.user = quietRun( 'who am i' ).split()[ 0 ] + if params.pop( 'precheck' ): + self.precheck() + self.connections = {} + self.placement = params.pop( 'placement', SwitchBinPlacer ) + # Make sure control directory exists + self.cdir = os.environ[ 'HOME' ] + '/.ssh/mn' + errRun( [ 'mkdir', '-p', self.cdir ] ) + Mininet.__init__( self, *args, **params ) + + def popen( self, cmd ): + "Popen() for server connections" + old = signal( SIGINT, SIG_IGN ) + conn = Popen( cmd, stdin=PIPE, stdout=PIPE, close_fds=True ) + signal( SIGINT, old ) + return conn + + def baddLink( self, *args, **kwargs ): + "break addlink for testing" + pass + + def precheck( self ): + """Pre-check to make sure connection works and that + we can call sudo without a password""" + result = 0 + info( '*** Checking servers\n' ) + for server in self.servers: + ip = self.serverIP[ server ] + if not server or server == 'localhost': + continue + info( server, '' ) + dest = '%s@%s' % ( self.user, ip ) + cmd = [ 'sudo', '-E', '-u', self.user ] + cmd += self.sshcmd + [ '-n', dest, 'sudo true' ] + debug( ' '.join( cmd ), '\n' ) + out, err, code = errRun( cmd ) + if code != 0: + error( '\nstartConnection: server connection check failed ' + 'to %s using command:\n%s\n' + % ( server, ' '.join( cmd ) ) ) + result |= code + if result: + error( '*** Server precheck failed.\n' + '*** Make sure that the above ssh command works correctly.\n' + '*** You may also need to run mn -c on all nodes, and/or\n' + '*** use sudo -E.\n' ) + exit( 1 ) + info( '\n' ) + + def modifiedaddHost( self, *args, **kwargs ): + "Slightly modify addHost" + kwargs[ 'splitInit' ] = True + return Mininet.addHost( *args, **kwargs ) + + + def placeNodes( self ): + """Place nodes on servers (if they don't have a server), and + start shell processes""" + if not self.servers or not self.topo: + # No shirt, no shoes, no service + return + nodes = self.topo.nodes() + placer = self.placement( servers=self.servers, + nodes=self.topo.nodes(), + hosts=self.topo.hosts(), + switches=self.topo.switches(), + links=self.topo.links() ) + for node in nodes: + config = self.topo.node_info[ node ] + server = config.setdefault( 'server', placer.place( node ) ) + if server: + config.setdefault( 'serverIP', self.serverIP[ server ] ) + info( '%s:%s ' % ( node, server ) ) + key = ( None, server ) + _dest, cfile, _conn = self.connections.get( + key, ( None, None, None ) ) + if cfile: + config.setdefault( 'controlPath', cfile ) + + def addController( self, *args, **kwargs ): + "Patch to update IP address to global IP address" + controller = Mininet.addController( self, *args, **kwargs ) + # Update IP address for controller that may not be local + if ( isinstance( controller, Controller) + and controller.IP() == '127.0.0.1' + and ' eth0:' in controller.cmd( 'ip link show' ) ): + Intf( 'eth0', node=controller ).updateIP() + return controller + + def buildFromTopo( self, *args, **kwargs ): + "Start network" + info( '*** Placing nodes\n' ) + self.placeNodes() + info( '\n' ) + Mininet.buildFromTopo( self, *args, **kwargs ) + + +def testNsTunnels(): + "Test tunnels between nodes in namespaces" + net = Mininet( host=RemoteHost, link=RemoteLink ) + h1 = net.addHost( 'h1' ) + h2 = net.addHost( 'h2', server='ubuntu2' ) + net.addLink( h1, h2 ) + net.start() + net.pingAll() + net.stop() + +# Manual topology creation with net.add*() +# +# This shows how node options may be used to manage +# cluster placement using the net.add*() API + +def testRemoteNet( remote='ubuntu2' ): + "Test remote Node classes" + print '*** Remote Node Test' + net = Mininet( host=RemoteHost, switch=RemoteOVSSwitch, + link=RemoteLink ) + c0 = net.addController( 'c0' ) + # Make sure controller knows its non-loopback address + Intf( 'eth0', node=c0 ).updateIP() + print "*** Creating local h1" + h1 = net.addHost( 'h1' ) + print "*** Creating remote h2" + h2 = net.addHost( 'h2', server=remote ) + print "*** Creating local s1" + s1 = net.addSwitch( 's1' ) + print "*** Creating remote s2" + s2 = net.addSwitch( 's2', server=remote ) + print "*** Adding links" + net.addLink( h1, s1 ) + net.addLink( s1, s2 ) + net.addLink( h2, s2 ) + net.start() + print 'Mininet is running on', quietRun( 'hostname' ).strip() + for node in c0, h1, h2, s1, s2: + print 'Node', node, 'is running on', node.cmd( 'hostname' ).strip() + net.pingAll() + CLI( net ) + net.stop() + + +# High-level/Topo API example +# +# This shows how existing Mininet topologies may be used in cluster +# mode by creating node placement functions and a controller which +# can be accessed remotely. This implements a very compatible version +# of cluster edition with a minimum of code! + +remoteHosts = [ 'h2' ] +remoteSwitches = [ 's2' ] +remoteServer = 'ubuntu2' + +def HostPlacer( name, *args, **params ): + "Custom Host() constructor which places hosts on servers" + if name in remoteHosts: + return RemoteHost( name, *args, server=remoteServer, **params ) + else: + return Host( name, *args, **params ) + +def SwitchPlacer( name, *args, **params ): + "Custom Switch() constructor which places switches on servers" + if name in remoteSwitches: + return RemoteOVSSwitch( name, *args, server=remoteServer, **params ) + else: + return RemoteOVSSwitch( name, *args, **params ) + +def ClusterController( *args, **kwargs): + "Custom Controller() constructor which updates its eth0 IP address" + controller = Controller( *args, **kwargs ) + # Find out its IP address so that cluster switches can connect + Intf( 'eth0', node=controller ).updateIP() + return controller + +def testRemoteTopo(): + "Test remote Node classes using Mininet()/Topo() API" + topo = LinearTopo( 2 ) + net = Mininet( topo=topo, host=HostPlacer, switch=SwitchPlacer, + link=RemoteLink, controller=ClusterController ) + net.start() + net.pingAll() + net.stop() + +# Need to test backwards placement, where each host is on +# a server other than its switch!! But seriously we could just +# do random switch placement rather than completely random +# host placement. + +def testRemoteSwitches(): + "Test with local hosts and remote switches" + servers = [ 'localhost', 'ubuntu2'] + topo = TreeTopo( depth=4, fanout=2 ) + net = MininetCluster( topo=topo, servers=servers, + placement=RoundRobinPlacer ) + net.start() + net.pingAll() + net.stop() + + +# +# For testing and demo purposes it would be nice to draw the +# network graph and color it based on server. + +# The MininetCluster() class integrates pluggable placement +# functions, for maximum ease of use. MininetCluster() also +# pre-flights and multiplexes server connections. + +def testMininetCluster(): + "Test MininetCluster()" + servers = [ 'localhost', 'ubuntu2' ] + topo = TreeTopo( depth=3, fanout=3 ) + net = MininetCluster( topo=topo, servers=servers, + placement=SwitchBinPlacer ) + net.start() + net.pingAll() + net.stop() + +def signalTest(): + "Make sure hosts are robust to signals" + h = RemoteHost( 'h0', server='ubuntu1' ) + h.shell.send_signal( SIGINT ) + h.shell.poll() + if h.shell.returncode is None: + print 'OK: ', h, 'has not exited' + else: + print 'FAILURE:', h, 'exited with code', h.shell.returncode + h.stop() + +if __name__ == '__main__': + setLogLevel( 'info' ) + # testRemoteTopo() + # testRemoteNet() + # testMininetCluster() + # testRemoteSwitches() + signalTest() diff --git a/examples/clustercli.py b/examples/clustercli.py new file mode 100644 index 0000000000000000000000000000000000000000..264f901c6d6acc9daa3314fc088a97342ab767f4 --- /dev/null +++ b/examples/clustercli.py @@ -0,0 +1,93 @@ +#!/usr/bin/python + +"CLI for Mininet Cluster Edition prototype demo" + +from mininet.cli import CLI +from mininet.log import output, error + +nx, graphviz_layout, plt = None, None, None # Will be imported on demand + + +class DemoCLI( CLI ): + "CLI with additional commands for Cluster Edition demo" + + @staticmethod + def colorsFor( seq ): + "Return a list of background colors for a sequence" + colors = [ 'red', 'lightgreen', 'cyan', 'yellow', 'orange', + 'magenta', 'pink', 'grey', 'brown', + 'white' ] + slen, clen = len( seq ), len( colors ) + reps = max( 1, slen / clen ) + colors = colors * reps + colors = colors[ 0 : slen ] + return colors + + def do_plot( self, line ): + "Plot topology colored by node placement" + # Import networkx if needed + global nx, plt + if not nx: + try: + import networkx as nx + import matplotlib.pyplot as plt + import pygraphviz + except: + error( 'plot requires networkx, matplotlib and pygraphviz - ' + 'please install them and try again\n' ) + return + # Make a networkx Graph + g = nx.Graph() + mn = self.mn + servers, hosts, switches = mn.servers, mn.hosts, mn.switches + hlen, slen = len( hosts ), len( switches ) + nodes = hosts + switches + g.add_nodes_from( nodes ) + links = [ ( link.intf1.node, link.intf2.node ) + for link in self.mn.links ] + g.add_edges_from( links ) + # Pick some shapes and colors + # shapes = hlen * [ 's' ] + slen * [ 'o' ] + color = dict( zip( servers, self.colorsFor( servers ) ) ) + # Plot it! + pos = nx.graphviz_layout( g ) + opts = { 'ax': None, 'font_weight': 'bold', + 'width': 2, 'edge_color': 'darkblue' } + hcolors = [ color[ h.server ] for h in hosts ] + scolors = [ color[ s.server ] for s in switches ] + nx.draw_networkx( g, pos=pos, nodelist=hosts, node_size=800, label='host', + node_color=hcolors, node_shape='s', **opts ) + nx.draw_networkx( g, pos=pos, nodelist=switches, node_size=1000, + node_color=scolors, node_shape='o', **opts ) + # Get rid of axes, add title, and show + fig = plt.gcf() + ax = plt.gca() + ax.get_xaxis().set_visible( False ) + ax.get_yaxis().set_visible( False ) + fig.canvas.set_window_title( 'Mininet') + plt.title( 'Node Placement', fontweight='bold' ) + plt.show() + + def do_status( self, line ): + "Report on node shell status" + nodes = self.mn.hosts + self.mn.switches + for node in nodes: + node.shell.poll() + exited = [ node for node in nodes + if node.shell.returncode is not None ] + if exited: + for node in exited: + output( '%s has exited with code %d\n' + % ( node, node.shell.returncode ) ) + else: + output( 'All nodes are still running.\n' ) + + + def do_placement( self, line ): + "Describe node placement" + mn = self.mn + nodes = mn.hosts + mn.switches + mn.controllers + for server in mn.servers: + names = [ n.name for n in nodes if hasattr( n, 'server' ) + and n.server == server ] + output( '%s: %s\n' % ( server, ' '.join( names ) ) ) diff --git a/examples/clusterdemo.py b/examples/clusterdemo.py new file mode 100755 index 0000000000000000000000000000000000000000..2e90b0fececd8f09f8e637ca5a72cacbe918a12c --- /dev/null +++ b/examples/clusterdemo.py @@ -0,0 +1,23 @@ +#!/usr/bin/python + +"clusterdemo.py: demo of Mininet Cluster Edition prototype" + +from mininet.examples.cluster import MininetCluster, SwitchBinPlacer +from mininet.topolib import TreeTopo +from mininet.log import setLogLevel +from mininet.examples.clustercli import DemoCLI as CLI + +def demo(): + "Simple Demo of Cluster Mode" + servers = [ 'localhost', 'ubuntu2', 'ubuntu3' ] + topo = TreeTopo( depth=3, fanout=3 ) + net = MininetCluster( topo=topo, servers=servers, + placement=SwitchBinPlacer ) + net.start() + CLI( net ) + net.stop() + +if __name__ == '__main__': + setLogLevel( 'info' ) + demo() + diff --git a/mininet/clean.py b/mininet/clean.py index 49e32eaf24bd267e0bd889f6564e6dfe6e00f5fc..bc9d375ab3ba7786fd88f2115d85b01b04be1720 100755 --- a/mininet/clean.py +++ b/mininet/clean.py @@ -21,6 +21,21 @@ def sh( cmd ): info( cmd + '\n' ) return Popen( [ '/bin/sh', '-c', cmd ], stdout=PIPE ).communicate()[ 0 ] +def killprocs( pattern ): + "Reliably terminate processes matching a pattern (including args)" + sh( 'pkill -9 -f %s' % pattern ) + # Make sure they are gone + while True: + try: + pids = co( 'pgrep -f %s' % pattern ) + except: + pids = '' + if pids: + sh( 'pkill -f 9 mininet:' ) + sleep( .5 ) + else: + break + def cleanup(): """Clean up junk which might be left over from old runs; do fast stuff before slow dp and link removal!""" @@ -70,17 +85,11 @@ def cleanup(): sh( "ip link del " + link ) info( "*** Killing stale mininet node processes\n" ) - sh( 'pkill -9 -f mininet:' ) - # Make sure they are gone - while True: - try: - pids = co( 'pgrep -f mininet:'.split() ) - except: - pids = '' - if pids: - sh( 'pkill -f 9 mininet:' ) - sleep( .5 ) - else: - break + killprocs( 'mininet:' ) + info ( "*** Shutting down stale tunnels\n" ) + killprocs( 'Tunnel=Ethernet' ) + killprocs( '.ssh/mn') + sh( 'rm -f ~/.ssh/mn/*' ) + info( "*** Cleanup complete.\n" ) diff --git a/mininet/cli.py b/mininet/cli.py index 42a4d3d7f1d6724d916475d3f17d6766098802a7..f6d752124e4c304b5c628f392cb88cb79f07f8ad 100644 --- a/mininet/cli.py +++ b/mininet/cli.py @@ -75,7 +75,7 @@ def __init__( self, mininet, stdin=sys.stdin, script=None ): for node in self.mn.values(): while node.waiting: node.sendInt() - node.monitor() + node.waitOutput() if self.isatty(): quietRun( 'stty echo sane intr "^C"' ) self.cmdloop() @@ -331,6 +331,11 @@ def do_time( self, line ): elapsed = time.time() - start self.stdout.write("*** Elapsed time: %0.6f secs\n" % elapsed) + def do_links( self, line ): + "Report on links" + for link in self.mn.links: + print link, link.status() + def default( self, line ): """Called on an input line when the command prefix is not recognized. Overridden to run shell commands when a node is the first CLI argument. diff --git a/mininet/link.py b/mininet/link.py index 74b9821661587bb64a9c78d8cf220ac8952b1a36..efb802e971c58c95c9e4e95cfcd2987bf24b141d 100644 --- a/mininet/link.py +++ b/mininet/link.py @@ -32,7 +32,8 @@ class Intf( object ): "Basic interface object that can configure itself." - def __init__( self, name, node=None, port=None, link=None, mac=None, **params ): + def __init__( self, name, node=None, port=None, link=None, + mac=None, srcNode=None, **params ): """name: interface name (e.g. h1-eth0) node: owning node (where this intf most likely lives) link: parent link if we're part of a link @@ -189,6 +190,14 @@ def delete( self ): # Link may have been dumped into root NS quietRun( 'ip link del ' + self.name ) + def status( self ): + "Return intf status as a string" + links, err_, result_ = self.node.pexec( 'ip link show' ) + if self.name in links: + return "OK" + else: + return "MISSING" + def __repr__( self ): return '<%s %s>' % ( self.__class__.__name__, self.name ) @@ -367,16 +376,27 @@ def __init__( self, node1, node2, port1=None, port2=None, params1: parameters for interface 1 params2: parameters for interface 2""" # This is a bit awkward; it seems that having everything in - # params would be more orthogonal, but being able to specify - # in-line arguments is more convenient! - if port1 is None: - port1 = node1.newPort() - if port2 is None: - port2 = node2.newPort() + # params is more orthogonal, but being able to specify + # in-line arguments is more convenient! So we support both. + if params1 is None: + params1 = {} + if params2 is None: + params2 = {} + # Allow passing in params1=params2 + if params2 is params1: + params2 = dict( params1 ) + if port1 is not None: + params1[ 'port' ] = port1 + if port2 is not None: + params2[ 'port' ] = port2 + if 'port' not in params1: + params1[ 'port' ] = node1.newPort() + if 'port' not in params2: + params2[ 'port' ] = node2.newPort() if not intfName1: - intfName1 = self.intfName( node1, port1 ) + intfName1 = self.intfName( node1, params1[ 'port' ] ) if not intfName2: - intfName2 = self.intfName( node2, port2 ) + intfName2 = self.intfName( node2, params2[ 'port' ] ) self.makeIntfPair( intfName1, intfName2, addr1, addr2 ) @@ -384,38 +404,41 @@ def __init__( self, node1, node2, port1=None, port2=None, cls1 = intf if not cls2: cls2 = intf - if not params1: - params1 = {} - if not params2: - params2 = {} - intf1 = cls1( name=intfName1, node=node1, port=port1, + intf1 = cls1( name=intfName1, node=node1, link=self, mac=addr1, **params1 ) - intf2 = cls2( name=intfName2, node=node2, port=port2, + intf2 = cls2( name=intfName2, node=node2, link=self, mac=addr2, **params2 ) # All we are is dust in the wind, and our two interfaces self.intf1, self.intf2 = intf1, intf2 - @classmethod - def intfName( cls, node, n ): + def intfName( _self, node, n ): "Construct a canonical interface name node-ethN for interface n." return node.name + '-eth' + repr( n ) @classmethod - def makeIntfPair( cls, intf1, intf2, addr1=None, addr2=None ): + def makeIntfPair( _cls, intfname1, intfname2, addr1=None, addr2=None ): """Create pair of interfaces - intf1: name of interface 1 - intf2: name of interface 2 - (override this class method [and possibly delete()] + intfname1: name of interface 1 + intfname2: name of interface 2 + (override this method [and possibly delete()] to change link type)""" - makeIntfPair( intf1, intf2, addr1, addr2 ) + return makeIntfPair( intfname1, intfname2, addr1, addr2 ) def delete( self ): "Delete this link" self.intf1.delete() self.intf2.delete() + def stop( self ): + "Override to stop and clean up link as needed" + pass + + def status( self ): + "Return link status as a string" + return "(%s %s)" % ( self.intf1.status(), self.intf2.status() ) + def __str__( self ): return '%s<->%s' % ( self.intf1, self.intf2 ) @@ -430,4 +453,4 @@ def __init__( self, node1, node2, port1=None, port2=None, cls2=TCIntf, addr1=addr1, addr2=addr2, params1=params, - params2=params) + params2=params ) diff --git a/mininet/net.py b/mininet/net.py index 8a6acdcc3a71a3546d65d96329053680a9880a2b..880fe045a9a7e3e43ef6abd8418375b1063367d4 100755 --- a/mininet/net.py +++ b/mininet/net.py @@ -156,6 +156,7 @@ def __init__( self, topo=None, switch=OVSKernelSwitch, host=Host, self.hosts = [] self.switches = [] self.controllers = [] + self.links = [] self.nameToNode = {} # name to Node (Host/Switch) objects @@ -337,7 +338,9 @@ def addLink( self, node1, node2, port1=None, port2=None, defaults.update( params ) if not cls: cls = self.link - return cls( node1, node2, **defaults ) + link = cls( node1, node2, **defaults ) + self.links.append( link ) + return link def configHosts( self ): "Configure a set of hosts." @@ -477,6 +480,11 @@ def stop( self ): for switch in self.switches: info( switch.name + ' ' ) switch.stop() + switch.terminate() + info( '\n' ) + info( '*** Stopping %i links\n' % len( self.links ) ) + for link in self.links: + link.stop() info( '\n' ) info( '*** Stopping %i hosts\n' % len( self.hosts ) ) for host in self.hosts: diff --git a/mininet/node.py b/mininet/node.py index 20b18aae1b47ff7ff6c2cfe1572615c64201c7b4..aa8b33314bb147ecea6a5479571b70cbeaadcb91 100644 --- a/mininet/node.py +++ b/mininet/node.py @@ -80,8 +80,8 @@ def __init__( self, name, inNamespace=True, **params ): # Make sure class actually works self.checkSetup() - self.name = name - self.inNamespace = inNamespace + self.name = params.get( 'name', name ) + self.inNamespace = params.get( 'inNamespace', inNamespace ) # Stash configuration parameters for future reference self.params = params @@ -116,29 +116,28 @@ def fdToNode( cls, fd ): return node or cls.inToNode.get( fd ) # Command support via shell process in namespace - - def startShell( self ): + def startShell( self, mnopts=None ): "Start a shell process for running commands" if self.shell: - error( "%s: shell is already running" ) + error( "%s: shell is already running\n" % self.name ) return # mnexec: (c)lose descriptors, (d)etach from tty, # (p)rint pid, and run in (n)amespace - opts = '-cd' + opts = '-cd' if mnopts is None else mnopts if self.inNamespace: opts += 'n' # bash -m: enable job control, i: force interactive # -s: pass $* to shell, and make process easy to find in ps # prompt is set to sentinel chr( 127 ) - os.environ[ 'PS1' ] = chr( 127 ) - cmd = [ 'mnexec', opts, 'bash', '--norc', '-mis', 'mininet:' + self.name ] + cmd = [ 'mnexec', opts, 'env', 'PS1=' + chr( 127 ), + 'bash', '--norc', '-mis', 'mininet:' + self.name ] # Spawn a shell subprocess in a pseudo-tty, to disable buffering # in the subprocess and insulate it from signals (e.g. SIGINT) # received by the parent master, slave = pty.openpty() - self.shell = Popen( cmd, stdin=slave, stdout=slave, stderr=slave, + self.shell = self._popen( cmd, stdin=slave, stdout=slave, stderr=slave, close_fds=False ) - self.stdin = os.fdopen( master ) + self.stdin = os.fdopen( master, 'rw' ) self.stdout = self.stdin self.pid = self.shell.pid self.pollOut = select.poll() @@ -162,6 +161,12 @@ def startShell( self ): self.cmd( 'stty -echo' ) self.cmd( 'set +m' ) + def _popen( self, cmd, **params ): + """Internal method: spawn and return a process + cmd: command to run (list) + params: parameters to Popen()""" + return Popen( cmd, **params ) + def cleanup( self ): "Help python collect its garbage." # Intfs may end up in root NS @@ -206,7 +211,8 @@ def write( self, data ): def terminate( self ): "Send kill signal to Node and clean up after it." if self.shell: - os.killpg( self.pid, signal.SIGHUP ) + if self.shell.poll() is None: + os.killpg( self.shell.pid, signal.SIGHUP ) self.cleanup() def stop( self ): @@ -251,12 +257,14 @@ def sendCmd( self, *args, **kwargs ): def sendInt( self, intr=chr( 3 ) ): "Interrupt running command." + debug( 'sendInt: writing chr(%d)\n' % ord( intr ) ) self.write( intr ) def monitor( self, timeoutms=None, findPid=True ): """Monitor and return the output of a command. Set self.waiting to False if command has completed. - timeoutms: timeout in ms or None to wait indefinitely.""" + timeoutms: timeout in ms or None to wait indefinitely + findPid: look for PID from mnexec -p""" self.waitReadable( timeoutms ) data = self.read( 1024 ) pidre = r'\[\d+\] \d+\r\n' @@ -282,7 +290,7 @@ def monitor( self, timeoutms=None, findPid=True ): data = data.replace( chr( 127 ), '' ) return data - def waitOutput( self, verbose=False ): + def waitOutput( self, verbose=False, findPid=True ): """Wait for a command to complete. Completion is signaled by a sentinel character, ASCII(127) appearing in the output stream. Wait for the sentinel and return @@ -331,18 +339,19 @@ def popen( self, *args, **kwargs ): # popen( cmd, arg1, arg2... ) cmd = list( args ) # Attach to our namespace using mnexec -a - mncmd = defaults[ 'mncmd' ] - del defaults[ 'mncmd' ] - cmd = mncmd + cmd + cmd = defaults.pop( 'mncmd' ) + cmd # Shell requires a string, not a list! if defaults.get( 'shell', False ): cmd = ' '.join( cmd ) - return Popen( cmd, **defaults ) + popen = self._popen( cmd, **defaults ) + return popen def pexec( self, *args, **kwargs ): """Execute a command using popen returns: out, err, exitcode""" - popen = self.popen( *args, **kwargs) + popen = self.popen( *args, stdin=PIPE, stdout=PIPE, stderr=PIPE, + **kwargs ) + # Warning: this can fail with large numbers of fds! out, err = popen.communicate() exitcode = popen.wait() return out, err, exitcode @@ -361,20 +370,22 @@ def newPort( self ): return max( self.ports.values() ) + 1 return self.portBase - def addIntf( self, intf, port=None ): + def addIntf( self, intf, port=None, moveIntfFn=moveIntf ): """Add an interface. intf: interface - port: port number (optional, typically OpenFlow port number)""" + port: port number (optional, typically OpenFlow port number) + moveIntfFn: function to move interface (optional)""" if port is None: port = self.newPort() self.intfs[ port ] = intf self.ports[ intf ] = port self.nameToIntf[ intf.name ] = intf debug( '\n' ) - debug( 'added intf %s:%d to node %s\n' % ( intf, port, self.name ) ) + debug( 'added intf %s (%d) to node %s\n' % ( + intf, port, self.name ) ) if self.inNamespace: debug( 'moving', intf, 'into namespace for', self.name, '\n' ) - moveIntf( intf.name, self ) + moveIntfFn( intf.name, self ) def defaultIntf( self ): "Return interface for lowest port" @@ -1105,7 +1116,8 @@ def start( self, controllers ): intfs = ' '.join( '-- add-port %s %s ' % ( self, intf ) + '-- set Interface %s ' % intf + 'ofport_request=%s ' % self.ports[ intf ] - for intf in self.intfList() if not intf.IP() ) + for intf in self.intfList() + if self.ports[ intf ] and not intf.IP() ) clist = ' '.join( '%s:%s:%d' % ( c.protocol, c.IP(), c.port ) for c in controllers ) if self.listenPort: diff --git a/mininet/util.py b/mininet/util.py index 69d31273f21ed858d35aca3004c74f3e17d6597e..3ebe6a2fd1db0c6ae38f17f5976bd2ecaa628030 100644 --- a/mininet/util.py +++ b/mininet/util.py @@ -145,21 +145,22 @@ def isShellBuiltin( cmd ): # live in the root namespace and thus do not have to be # explicitly moved. -def makeIntfPair( intf1, intf2, addr1=None, addr2=None ): +def makeIntfPair( intf1, intf2, addr1=None, addr2=None, run=quietRun ): """Make a veth pair connecting intf1 and intf2. intf1: string, interface intf2: string, interface - returns: success boolean""" + node: node to run on or None (default) + returns: ip link add result""" # Delete any old interfaces with the same names - quietRun( 'ip link del ' + intf1 ) - quietRun( 'ip link del ' + intf2 ) + run( 'ip link del ' + intf1 ) + run( 'ip link del ' + intf2 ) # Create new pair if addr1 is None and addr2 is None: cmd = 'ip link add name ' + intf1 + ' type veth peer name ' + intf2 else: cmd = ( 'ip link add name ' + intf1 + ' address ' + addr1 + ' type veth peer name ' + intf2 + ' address ' + addr2 ) - cmdOutput = quietRun( cmd ) + cmdOutput = run( cmd ) if cmdOutput == '': return True else: @@ -180,36 +181,32 @@ def retry( retries, delaySecs, fn, *args, **keywords ): error( "*** gave up after %i retries\n" % tries ) exit( 1 ) -def moveIntfNoRetry( intf, dstNode, srcNode=None, printError=False ): +def moveIntfNoRetry( intf, dstNode, printError=False ): """Move interface to node, without retrying. intf: string, interface dstNode: destination Node - srcNode: source Node or None (default) for root ns printError: if true, print error""" intf = str( intf ) cmd = 'ip link set %s netns %s' % ( intf, dstNode.pid ) - if srcNode: - cmdOutput = srcNode.cmd( cmd ) - else: - cmdOutput = quietRun( cmd ) + cmdOutput = quietRun( cmd ) # If ip link set does not produce any output, then we can assume # that the link has been moved successfully. if cmdOutput: if printError: error( '*** Error: moveIntf: ' + intf + - ' not successfully moved to ' + dstNode.name + '\n' ) + ' not successfully moved to ' + dstNode.name + ':\n', + cmdOutput ) return False return True -def moveIntf( intf, dstNode, srcNode=None, printError=False, +def moveIntf( intf, dstNode, srcNode=None, printError=True, retries=3, delaySecs=0.001 ): """Move interface to node, retrying on failure. intf: string, interface dstNode: destination Node - srcNode: source Node or None (default) for root ns printError: if true, print error""" retry( retries, delaySecs, moveIntfNoRetry, intf, dstNode, - srcNode=srcNode, printError=printError ) + printError=printError ) # Support for dumping network diff --git a/util/clustersetup.sh b/util/clustersetup.sh new file mode 100755 index 0000000000000000000000000000000000000000..89d5cff65dc6e3253db670c8df609317d1b9a068 --- /dev/null +++ b/util/clustersetup.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env bash + +# Mininet ssh authentication script for cluster edition +# This script will create a single key pair, which is then +# propagated throughout the entire cluster. +# There are two options for setup; temporary setup +# persistent setup. If no options are specified, and the script +# is only given ip addresses or host names, it will default to +# the temporary setup. An ssh directory is then created in +# /tmp/mn/ssh on each node, and mounted with the keys over the +# user's ssh directory. This setup can easily be torn down by running +# clustersetup with the -c option. +# If the -p option is used, the setup will be persistent. In this +# case, the key pair will be be distributed directly to each node's +# ssh directory, but will be called cluster_key. An option to +# specify this key for use will be added to the config file in each +# user's ssh directory. + + +set -e +num_options=0 +persistent=false +showHelp=false +clean=false +declare -a hosts=() +user=$(whoami) +SSHDIR=/tmp/mn/ssh +USERDIR=/home/$user/.ssh +usage=$'./clustersetup.sh [ -p|h|c ] [ host1 ] [ host2 ] ...\n + Authenticate yourself and other cluster nodes to each other + via ssh for mininet cluster edition. By default, we use a + temporary ssh setup. An ssh directory is mounted over + /home/user/.ssh on each machine in the cluster. + + -h: display this help + -p: create a persistent ssh setup. This will add + new ssh keys and known_hosts to each nodes + /home/user/.ssh directory + -c: method to clean up a temporary ssh setup. + Any hosts taken as arguments will be cleaned + ' + +persistentSetup() { + echo "***creating key pair" + ssh-keygen -t rsa -C "Cluster_Edition_Key" -f $USERDIR/cluster_key -N '' &> /dev/null + cat $USERDIR/cluster_key.pub >> $USERDIR/authorized_keys + echo "***configuring ssh" + echo "IdentityFile $USERDIR/cluster_key" >> $USERDIR/config + echo "IdentityFile $USERDIR/id_rsa" >> $USERDIR/config + + for host in $hosts; do + echo "***copying public key to $host" + ssh-copy-id -i $USERDIR/cluster_key.pub $user@$host &> /dev/null + echo "***copying key pair to remote host" + scp $USERDIR/cluster_key $user@$host:$USERDIR + scp $USERDIR/cluster_key.pub $user@$host:$USERDIR + echo "***configuring remote host" + ssh -o ForwardAgent=yes $user@$host " + echo 'IdentityFile $USERDIR/cluster_key' >> $USERDIR/config + echo 'IdentityFile $USERDIR/id_rsa' >> $USERDIR/config" + done + + for host in $hosts; do + echo "***copying known_hosts to $host" + scp $USERDIR/known_hosts $user@$host:$USERDIR/cluster_known_hosts + ssh $user@$host " + cat $USERDIR/cluster_known_hosts >> $USERDIR/known_hosts + rm $USERDIR/cluster_known_hosts" + done +} + +tempSetup() { + + echo "***creating temporary ssh directory" + mkdir -p $SSHDIR + echo "***creating key pair" + ssh-keygen -t rsa -C "Cluster_Edition_Key" -f /tmp/mn/ssh/id_rsa -N '' &> /dev/null + + echo "***mounting temporary ssh directory" + sudo mount --bind $SSHDIR /home/$user/.ssh + cp $SSHDIR/id_rsa.pub $SSHDIR/authorized_keys + +for host in $hosts; do + echo "***copying public key to $host" + ssh-copy-id $user@$host &> /dev/null + echo "***mounting remote temporary ssh directory for $host" + ssh -o ForwardAgent=yes $user@$host " + mkdir -p /tmp/mn/ssh + cp /home/$user/.ssh/authorized_keys $SSHDIR/authorized_keys + sudo mount --bind $SSHDIR /home/$user/.ssh" + echo "***copying key pair to $host" + scp $SSHDIR/{id_rsa,id_rsa.pub} $user@$host:$SSHDIR +done + +for host in $hosts; do + echo "***copying known_hosts to $host" + scp $SSHDIR/known_hosts $user@$host:$SSHDIR +done +} + +cleanup() { + + for host in $hosts; do + echo "***cleaning up $host" + ssh $user@$host "sudo umount /home/$user/.ssh + sudo rm -rf $SSHDIR" + done + + echo "**unmounting local directories" + sudo umount /home/$user/.ssh + echo "***removing temporary ssh directory" + sudo rm -rf $SSHDIR + echo "done!" + +} + + +if [ $# -eq 0 ]; then + echo "ERROR: No Arguments" + echo "$usage" + exit +else + while getopts 'hpc' OPTION + do + ((num_options+=1)) + case $OPTION in + h) showHelp=true;; + p) persistent=true;; + c) clean=true;; + ?) showHelp=true;; + esac + done + shift $(($OPTIND - 1)) +fi + +if [ "$num_options" -gt 1 ]; then + echo "ERROR: Too Many Options" + echo "$usage" + exit +fi + +if $showHelp; then + echo "$usage" + exit +fi + +for i in "$@"; do + output=$(getent ahostsv4 "$i") + if [ -z "$output" ]; then + echo '***WARNING: could not find hostname "$i"' + echo "" + else + hosts+="$i " + fi +done + +if $clean; then + cleanup + exit +fi + +echo "***authenticating to:" +for host in $hosts; do + echo "$host" +done + +echo + +if $persistent; then + echo '***Setting up persistent SSH configuration between all nodes' + persistentSetup + echo $'\n*** Sucessfully set up ssh throughout the cluster!' + +else + echo '*** Setting up temporary SSH configuration between all nodes' + tempSetup + echo $'\n***Finished temporary setup. When you are done with your cluster' + echo $' session, tear down the SSH connections with' + echo $' ./clustersetup.sh -c '$hosts'' +fi + +echo