""" Mininet: A simple networking testbed for OpenFlow/SDN! author: Bob Lantz (rlantz@cs.stanford.edu) author: Brandon Heller (brandonh@stanford.edu) Mininet creates scalable OpenFlow test networks by using process-based virtualization and network namespaces. Simulated hosts are created as processes in separate network namespaces. This allows a complete OpenFlow network to be simulated on top of a single Linux kernel. Each host has: A virtual console (pipes to a shell) A virtual interfaces (half of a veth pair) A parent shell (and possibly some child processes) in a namespace Hosts have a network interface which is configured via ifconfig/ip link/etc. This version supports both the kernel and user space datapaths from the OpenFlow reference implementation (openflowswitch.org) as well as OpenVSwitch (openvswitch.org.) In kernel datapath mode, the controller and switches are simply processes in the root namespace. Kernel OpenFlow datapaths are instantiated using dpctl(8), and are attached to the one side of a veth pair; the other side resides in the host namespace. In this mode, switch processes can simply connect to the controller via the loopback interface. In user datapath mode, the controller and switches can be full-service nodes that live in their own network namespaces and have management interfaces and IP addresses on a control network (e.g. 192.168.123.1, currently routed although it could be bridged.) In addition to a management interface, user mode switches also have several switch interfaces, halves of veth pairs whose other halves reside in the host nodes that the switches are connected to. Consistent, straightforward naming is important in order to easily identify hosts, switches and controllers, both from the CLI and from program code. Interfaces are named to make it easy to identify which interfaces belong to which node. The basic naming scheme is as follows: Host nodes are named h1-hN Switch nodes are named s1-sN Controller nodes are named c0-cN Interfaces are named {nodename}-eth0 .. {nodename}-ethN Note: If the network topology is created using mininet.topo, then node numbers are unique among hosts and switches (e.g. we have h1..hN and SN..SN+M) and also correspond to their default IP addresses of 10.x.y.z/8 where x.y.z is the base-256 representation of N for hN. This mapping allows easy determination of a node's IP address from its name, e.g. h1 -> 10.0.0.1, h257 -> 10.0.1.1. Note also that 10.0.0.1 can often be written as 10.1 for short, e.g. "ping 10.1" is equivalent to "ping 10.0.0.1". Currently we wrap the entire network in a 'mininet' object, which constructs a simulated network based on a network topology created using a topology object (e.g. LinearTopo) from mininet.topo or mininet.topolib, and a Controller which the switches will connect to. Several configuration options are provided for functions such as automatically setting MAC addresses, populating the ARP table, or even running a set of terminals to allow direct interaction with nodes. After the network is created, it can be started using start(), and a variety of useful tasks maybe performed, including basic connectivity and bandwidth tests and running the mininet CLI. Once the network is up and running, test code can easily get access to host and switch objects which can then be used for arbitrary experiments, typically involving running a series of commands on the hosts. After all desired tests or activities have been completed, the stop() method may be called to shut down the network. """ import os import re import select import signal import random from sys import exit # pylint: disable=redefined-builtin from time import sleep from itertools import chain, groupby from math import ceil from mininet.cli import CLI from mininet.log import info, error, output, warn, debug from mininet.node import ( Node, Host, OVSKernelSwitch, DefaultController, Controller ) from mininet.nodelib import NAT from mininet.link import Link, Intf from mininet.util import ( quietRun, fixLimits, numCores, ensureRoot, macColonHex, ipStr, ipParse, netParse, ipAdd, waitListening, BaseString, fmtBps ) from mininet.term import cleanUpScreens, makeTerms # Mininet version: should be consistent with README and LICENSE VERSION = "2.3.1b4" class Mininet( object ): "Network emulation with hosts spawned in network namespaces." # pylint: disable=too-many-arguments def __init__( self, topo=None, switch=OVSKernelSwitch, host=Host, controller=DefaultController, link=Link, intf=Intf, build=True, xterms=False, cleanup=False, ipBase='10.0.0.0/8', inNamespace=False, autoSetMacs=False, autoStaticArp=False, autoPinCpus=False, listenPort=None, waitConnected=False ): """Create Mininet object. topo: Topo (topology) object or None switch: default Switch class host: default Host class/constructor controller: default Controller class/constructor link: default Link class/constructor intf: default Intf class/constructor ipBase: base IP address for hosts, build: build now from topo? xterms: if build now, spawn xterms? cleanup: if build now, cleanup before creating? inNamespace: spawn switches and controller in net namespaces? autoSetMacs: set MAC addrs automatically like IP addresses? autoStaticArp: set all-pairs static MAC addrs? autoPinCpus: pin hosts to (real) cores (requires CPULimitedHost)? listenPort: base listening port to open; will be incremented for each additional switch in the net if inNamespace=False waitConnected: wait for switches to Connect? (False; True/None=wait indefinitely; time(s)=timed wait)""" self.topo = topo self.switch = switch self.host = host self.controller = controller self.link = link self.intf = intf self.ipBase = ipBase self.ipBaseNum, self.prefixLen = netParse( self.ipBase ) hostIP = ( 0xffffffff >> self.prefixLen ) & self.ipBaseNum # Start for address allocation self.nextIP = hostIP if hostIP > 0 else 1 self.inNamespace = inNamespace self.xterms = xterms self.cleanup = cleanup self.autoSetMacs = autoSetMacs self.autoStaticArp = autoStaticArp self.autoPinCpus = autoPinCpus self.numCores = numCores() self.nextCore = 0 # next core for pinning hosts to CPUs self.listenPort = listenPort self.waitConn = waitConnected self.hosts = [] self.switches = [] self.controllers = [] self.links = [] self.nameToNode = {} # name to Node (Host/Switch) objects self.terms = [] # list of spawned xterm processes Mininet.init() # Initialize Mininet if necessary self.built = False if topo and build: self.build() def waitConnected( self, timeout=None, delay=.5 ): """wait for each switch to connect to a controller timeout: time to wait, or None or True to wait indefinitely delay: seconds to sleep per iteration returns: True if all switches are connected""" info( '*** Waiting for switches to connect\n' ) time = 0.0 remaining = list( self.switches ) # False: 0s timeout; None: wait forever (preserve 2.2 behavior) if isinstance( timeout, bool ): timeout = None if timeout else 0 while True: for switch in tuple( remaining ): if switch.connected(): info( '%s ' % switch ) remaining.remove( switch ) if not remaining: info( '\n' ) return True if timeout is not None and time >= timeout: break sleep( delay ) time += delay warn( 'Timed out after %d seconds\n' % time ) for switch in remaining.copy(): if not switch.connected(): warn( 'Warning: %s is not connected to a controller\n' % switch.name ) else: remaining.remove( switch ) return not remaining def addHost( self, name, cls=None, **params ): """Add host. name: name of host to add cls: custom host class/constructor (optional) params: parameters for host returns: added host""" # Default IP and MAC addresses defaults = { 'ip': ipAdd( self.nextIP, ipBaseNum=self.ipBaseNum, prefixLen=self.prefixLen ) + '/%s' % self.prefixLen } if self.autoSetMacs: defaults[ 'mac' ] = macColonHex( self.nextIP ) if self.autoPinCpus: defaults[ 'cores' ] = self.nextCore self.nextCore = ( self.nextCore + 1 ) % self.numCores self.nextIP += 1 defaults.update( params ) if not cls: cls = self.host h = cls( name, **defaults ) self.hosts.append( h ) self.nameToNode[ name ] = h return h def delNode( self, node, nodes=None): """Delete node node: node to delete nodes: optional list to delete from (e.g. self.hosts)""" if nodes is None: nodes = ( self.hosts if node in self.hosts else ( self.switches if node in self.switches else ( self.controllers if node in self.controllers else [] ) ) ) node.stop( deleteIntfs=True ) node.terminate() nodes.remove( node ) del self.nameToNode[ node.name ] def delHost( self, host ): "Delete a host" self.delNode( host, nodes=self.hosts ) def addSwitch( self, name, cls=None, **params ): """Add switch. name: name of switch to add cls: custom switch class/constructor (optional) returns: added switch side effect: increments listenPort ivar .""" defaults = { 'listenPort': self.listenPort, 'inNamespace': self.inNamespace } defaults.update( params ) if not cls: cls = self.switch sw = cls( name, **defaults ) if not self.inNamespace and self.listenPort: self.listenPort += 1 self.switches.append( sw ) self.nameToNode[ name ] = sw return sw def delSwitch( self, switch ): "Delete a switch" self.delNode( switch, nodes=self.switches ) def addController( self, name='c0', controller=None, **params ): """Add controller. controller: Controller class""" # Get controller class if not controller: controller = self.controller # Construct new controller if one is not given if isinstance( name, Controller ): controller_new = name # Pylint thinks controller is a str() # pylint: disable=maybe-no-member name = controller_new.name # pylint: enable=maybe-no-member else: controller_new = controller( name, **params ) # Add new controller to net if controller_new: # allow controller-less setups self.controllers.append( controller_new ) self.nameToNode[ name ] = controller_new return controller_new def delController( self, controller ): """Delete a controller Warning - does not reconfigure switches, so they may still attempt to connect to it!""" self.delNode( controller ) def addNAT( self, name='nat0', connect=True, inNamespace=False, **params): """Add a NAT to the Mininet network name: name of NAT node connect: switch to connect to | True (s1) | None inNamespace: create in a network namespace params: other NAT node params, notably: ip: used as default gateway address""" nat = self.addHost( name, cls=NAT, inNamespace=inNamespace, subnet=self.ipBase, **params ) # find first switch and create link if connect: if not isinstance( connect, Node ): # Use first switch if not specified connect = self.switches[ 0 ] # Connect the nat to the switch self.addLink( nat, connect ) # Set the default route on hosts natIP = nat.params[ 'ip' ].split('/')[ 0 ] for host in self.hosts: if host.inNamespace: host.setDefaultRoute( 'via %s' % natIP ) return nat # BL: We now have four ways to look up nodes # This may (should?) be cleaned up in the future. def getNodeByName( self, *args ): "Return node(s) with given name(s)" if len( args ) == 1: return self.nameToNode[ args[ 0 ] ] return [ self.nameToNode[ n ] for n in args ] def get( self, *args ): "Convenience alias for getNodeByName" return self.getNodeByName( *args ) # Even more convenient syntax for node lookup and iteration def __getitem__( self, key ): "net[ name ] operator: Return node with given name" return self.nameToNode[ key ] def __delitem__( self, key ): "del net[ name ] operator - delete node with given name" self.delNode( self.nameToNode[ key ] ) def __iter__( self ): "return iterator over node names" for node in chain( self.hosts, self.switches, self.controllers ): yield node.name def __len__( self ): "returns number of nodes in net" return ( len( self.hosts ) + len( self.switches ) + len( self.controllers ) ) def __contains__( self, item ): "returns True if net contains named node" return item in self.nameToNode def keys( self ): "return a list of all node names or net's keys" return list( self ) def values( self ): "return a list of all nodes or net's values" return [ self[name] for name in self ] def items( self ): "return (key,value) tuple list for every node in net" return zip( self.keys(), self.values() ) @staticmethod def randMac(): "Return a random, non-multicast MAC address" return macColonHex( random.randint(1, 2**48 - 1) & 0xfeffffffffff | 0x020000000000 ) def addLink( self, node1, node2, port1=None, port2=None, cls=None, **params ): """"Add a link from node1 to node2 node1: source node (or name) node2: dest node (or name) port1: source port (optional) port2: dest port (optional) cls: link class (optional) params: additional link params (optional) returns: link object""" # Accept node objects or names node1 = node1 if not isinstance( node1, BaseString ) else self[ node1 ] node2 = node2 if not isinstance( node2, BaseString ) else self[ node2 ] options = dict( params ) # Port is optional if port1 is not None: options.setdefault( 'port1', port1 ) if port2 is not None: options.setdefault( 'port2', port2 ) if self.intf is not None: options.setdefault( 'intf', self.intf ) # Set default MAC - this should probably be in Link options.setdefault( 'addr1', self.randMac() ) options.setdefault( 'addr2', self.randMac() ) cls = self.link if cls is None else cls link = cls( node1, node2, **options ) self.links.append( link ) return link def delLink( self, link ): "Remove a link from this network" link.delete() self.links.remove( link ) def linksBetween( self, node1, node2 ): "Return Links between node1 and node2" return [ link for link in self.links if ( node1, node2 ) in ( ( link.intf1.node, link.intf2.node ), ( link.intf2.node, link.intf1.node ) ) ] def delLinkBetween( self, node1, node2, index=0, allLinks=False ): """Delete link(s) between node1 and node2 index: index of link to delete if multiple links (0) allLinks: ignore index and delete all such links (False) returns: deleted link(s)""" links = self.linksBetween( node1, node2 ) if not allLinks: links = [ links[ index ] ] for link in links: self.delLink( link ) return links def configHosts( self ): "Configure a set of hosts." for host in self.hosts: info( host.name + ' ' ) intf = host.defaultIntf() if intf: host.configDefault() else: # Don't configure nonexistent intf host.configDefault( ip=None, mac=None ) # You're low priority, dude! # BL: do we want to do this here or not? # May not make sense if we have CPU limiting... # quietRun( 'renice +18 -p ' + repr( host.pid ) ) # This may not be the right place to do this, but # it needs to be done somewhere. info( '\n' ) def buildFromTopo( self, topo=None ): """Build mininet from a topology object At the end of this function, everything should be connected and up.""" # Possibly we should clean up here and/or validate # the topo if self.cleanup: pass info( '*** Creating network\n' ) if not self.controllers and self.controller: # Add a default controller info( '*** Adding controller\n' ) classes = self.controller if not isinstance( classes, list ): classes = [ classes ] for i, cls in enumerate( classes ): # Allow Controller objects because nobody understands partial() if isinstance( cls, Controller ): self.addController( cls ) else: self.addController( 'c%d' % i, cls ) info( '*** Adding hosts:\n' ) for hostName in topo.hosts(): self.addHost( hostName, **topo.nodeInfo( hostName ) ) info( hostName + ' ' ) info( '\n*** Adding switches:\n' ) for switchName in topo.switches(): # A bit ugly: add batch parameter if appropriate params = topo.nodeInfo( switchName) cls = params.get( 'cls', self.switch ) if hasattr( cls, 'batchStartup' ): params.setdefault( 'batch', True ) self.addSwitch( switchName, **params ) info( switchName + ' ' ) info( '\n*** Adding links:\n' ) for srcName, dstName, params in topo.links( sort=True, withInfo=True ): self.addLink( **params ) info( '(%s, %s) ' % ( srcName, dstName ) ) info( '\n' ) def configureControlNetwork( self ): "Control net config hook: override in subclass" raise Exception( 'configureControlNetwork: ' 'should be overriden in subclass', self ) def build( self ): "Build mininet." if self.topo: self.buildFromTopo( self.topo ) if self.inNamespace: self.configureControlNetwork() info( '*** Configuring hosts\n' ) self.configHosts() if self.xterms: self.startTerms() if self.autoStaticArp: self.staticArp() self.built = True def startTerms( self ): "Start a terminal for each node." if 'DISPLAY' not in os.environ: error( "Error starting terms: Cannot connect to display\n" ) return info( "*** Running terms on %s\n" % os.environ[ 'DISPLAY' ] ) cleanUpScreens() self.terms += makeTerms( self.controllers, 'controller' ) self.terms += makeTerms( self.switches, 'switch' ) self.terms += makeTerms( self.hosts, 'host' ) def stopXterms( self ): "Kill each xterm." for term in self.terms: os.kill( term.pid, signal.SIGKILL ) cleanUpScreens() def staticArp( self ): "Add all-pairs ARP entries to remove the need to handle broadcast." for src in self.hosts: for dst in self.hosts: if src != dst: src.setARP( ip=dst.IP(), mac=dst.MAC() ) def start( self ): "Start controller and switches." if not self.built: self.build() info( '*** Starting controller\n' ) for controller in self.controllers: info( controller.name + ' ') controller.start() info( '\n' ) info( '*** Starting %s switches\n' % len( self.switches ) ) for switch in self.switches: info( switch.name + ' ') switch.start( self.controllers ) started = {} for swclass, switches in groupby( sorted( self.switches, key=lambda s: str( type( s ) ) ), type ): switches = tuple( switches ) if hasattr( swclass, 'batchStartup' ): success = swclass.batchStartup( switches ) started.update( { s: s for s in success } ) info( '\n' ) if self.waitConn: self.waitConnected( self.waitConn ) def stop( self ): "Stop the controller(s), switches and hosts" info( '*** Stopping %i controllers\n' % len( self.controllers ) ) for controller in self.controllers: info( controller.name + ' ' ) controller.stop() info( '\n' ) # Unlimit cfs hosts to speed up shutdown for h in self.hosts: if hasattr( h, 'unlimit' ): h.unlimit() if self.terms: info( '*** Stopping %i terms\n' % len( self.terms ) ) self.stopXterms() info( '*** Stopping %i links\n' % len( self.links ) ) for link in self.links: info( '.' ) link.stop() info( '\n' ) info( '*** Stopping %i switches\n' % len( self.switches ) ) stopped = {} for swclass, switches in groupby( sorted( self.switches, key=lambda s: str( type( s ) ) ), type ): switches = tuple( switches ) if hasattr( swclass, 'batchShutdown' ): success = swclass.batchShutdown( switches ) stopped.update( { s: s for s in success } ) for switch in self.switches: info( switch.name + ' ' ) if switch not in stopped: switch.stop() switch.terminate() info( '\n' ) info( '*** Stopping %i hosts\n' % len( self.hosts ) ) for host in self.hosts: info( host.name + ' ' ) host.terminate() info( '\n*** Done\n' ) def run( self, test, *args, **kwargs ): "Perform a complete start/test/stop cycle." self.start() info( '*** Running test\n' ) result = test( *args, **kwargs ) self.stop() return result def monitor( self, hosts=None, timeoutms=-1 ): """Monitor a set of hosts (or all hosts by default), and return their output, a line at a time. hosts: (optional) set of hosts to monitor timeoutms: (optional) timeout value in ms returns: iterator which returns host, line""" if hosts is None: hosts = self.hosts poller = select.poll() h1 = hosts[ 0 ] # so we can call class method fdToNode for host in hosts: poller.register( host.stdout ) while True: ready = poller.poll( timeoutms ) for fd, event in ready: host = h1.fdToNode( fd ) if event & select.POLLIN: line = host.readline() if line is not None: yield host, line # Return if non-blocking if not ready and timeoutms >= 0: yield None, None # XXX These test methods should be moved out of this class. # Probably we should create a tests.py for them @staticmethod def _parsePing( pingOutput ): "Parse ping output and return packets sent, received." # Check for downed link if 'connect: Network is unreachable' in pingOutput: return 1, 0 r = r'(\d+) packets transmitted, (\d+)( packets)? received' m = re.search( r, pingOutput ) if m is None: error( '*** Error: could not parse ping output: %s\n' % pingOutput ) return 1, 0 sent, received = int( m.group( 1 ) ), int( m.group( 2 ) ) return sent, received def ping( self, hosts=None, timeout=None ): """Ping between all specified hosts. hosts: list of hosts timeout: time to wait for a response, as string returns: ploss packet loss percentage""" # should we check if running? packets = 0 lost = 0 ploss = None if not hosts: hosts = self.hosts output( '*** Ping: testing ping reachability\n' ) for node in hosts: output( '%s -> ' % node.name ) for dest in hosts: if node != dest: opts = '' if timeout: opts = '-W %s' % timeout if dest.intfs: result = node.cmd( 'LANG=C ping -c1 %s %s' % (opts, dest.IP()) ) sent, received = self._parsePing( result ) else: sent, received = 0, 0 packets += sent if received > sent: error( '*** Error: received too many packets' ) error( '%s' % result ) node.cmdPrint( 'route' ) exit( 1 ) lost += sent - received output( ( '%s ' % dest.name ) if received else 'X ' ) output( '\n' ) if packets > 0: ploss = 100.0 * lost / packets received = packets - lost output( "*** Results: %i%% dropped (%d/%d received)\n" % ( ploss, received, packets ) ) else: ploss = 0 output( "*** Warning: No packets sent\n" ) return ploss @staticmethod def _parsePingFull( pingOutput ): "Parse ping output and return all data." errorTuple = (1, 0, 0, 0, 0, 0) # Check for downed link r = r'[uU]nreachable' m = re.search( r, pingOutput ) if m is not None: return errorTuple r = r'(\d+) packets transmitted, (\d+)( packets)? received' m = re.search( r, pingOutput ) if m is None: error( '*** Error: could not parse ping output: %s\n' % pingOutput ) return errorTuple sent, received = int( m.group( 1 ) ), int( m.group( 2 ) ) r = r'rtt min/avg/max/mdev = ' r += r'(\d+\.\d+)/(\d+\.\d+)/(\d+\.\d+)/(\d+\.\d+) ms' m = re.search( r, pingOutput ) if m is None: if received == 0: return errorTuple error( '*** Error: could not parse ping output: %s\n' % pingOutput ) return errorTuple rttmin = float( m.group( 1 ) ) rttavg = float( m.group( 2 ) ) rttmax = float( m.group( 3 ) ) rttdev = float( m.group( 4 ) ) return sent, received, rttmin, rttavg, rttmax, rttdev def pingFull( self, hosts=None, timeout=None ): """Ping between all specified hosts and return all data. hosts: list of hosts timeout: time to wait for a response, as string returns: all ping data; see function body.""" # should we check if running? # Each value is a tuple: (src, dsd, [all ping outputs]) all_outputs = [] if not hosts: hosts = self.hosts output( '*** Ping: testing ping reachability\n' ) for node in hosts: output( '%s -> ' % node.name ) for dest in hosts: if node != dest: opts = '' if timeout: opts = '-W %s' % timeout result = node.cmd( 'ping -c1 %s %s' % (opts, dest.IP()) ) outputs = self._parsePingFull( result ) sent, received, rttmin, rttavg, rttmax, rttdev = outputs all_outputs.append( (node, dest, outputs) ) output( ( '%s ' % dest.name ) if received else 'X ' ) output( '\n' ) output( "*** Results: \n" ) for outputs in all_outputs: src, dest, ping_outputs = outputs sent, received, rttmin, rttavg, rttmax, rttdev = ping_outputs output( " %s->%s: %s/%s, " % (src, dest, sent, received ) ) output( "rtt min/avg/max/mdev %0.3f/%0.3f/%0.3f/%0.3f ms\n" % (rttmin, rttavg, rttmax, rttdev) ) return all_outputs def pingAll( self, timeout=None ): """Ping between all hosts. returns: ploss packet loss percentage""" return self.ping( timeout=timeout ) def pingPair( self ): """Ping between first two hosts, useful for testing. returns: ploss packet loss percentage""" hosts = [ self.hosts[ 0 ], self.hosts[ 1 ] ] return self.ping( hosts=hosts ) def pingAllFull( self ): """Ping between all hosts. returns: ploss packet loss percentage""" return self.pingFull() def pingPairFull( self ): """Ping between first two hosts, useful for testing. returns: ploss packet loss percentage""" hosts = [ self.hosts[ 0 ], self.hosts[ 1 ] ] return self.pingFull( hosts=hosts ) @staticmethod def _iperfVals( iperfcsv, serverip ): """Return iperf CSV as dict iperfcsv: iperf -y C output serverip: iperf server IP address """ fields = 'date cip cport sip sport ipver interval sent rate' lines = iperfcsv.strip().split('\n') svals = {} for line in lines: if ',' not in line: continue line = line.split( ',' ) svals = dict( zip( fields.split(), line ) ) # Return client in cip:cport, server in sip:sport if svals[ 'cip' ] == serverip: svals[ 'cip' ], svals[ 'sip' ] = ( svals[ 'sip' ], svals[ 'cip' ] ) svals[ 'cport' ], svals[ 'sport' ] = ( svals[ 'sport' ], svals[ 'cport' ] ) return svals # XXX This should be cleaned up def iperf( self, hosts=None, l4Type='TCP', udpBw='10M', fmt=None, seconds=5, port=5001): """Run iperf between two hosts. hosts: list of hosts; if None, uses first and last hosts l4Type: string, one of [ TCP, UDP ] udpBw: bandwidth target for UDP test fmt: scale/format argument (e.g. m/M for Mbps) seconds: iperf time to transmit port: iperf port returns: two-element array of [ server, client ] speeds note: send() is buffered, so client rate can be much higher than the actual transmission rate; on an unloaded system, server rate should be much closer to the actual receive rate""" hosts = hosts or [ self.hosts[ 0 ], self.hosts[ -1 ] ] assert len( hosts ) == 2 client, server = hosts output( '*** Iperf: testing', l4Type, 'bandwidth between', client, 'and', server, '\n' ) server.cmd( 'killall -9 iperf' ) # Note: CSV mode iperfArgs = 'iperf -y C -p %d ' % port bwArgs = '' if l4Type == 'UDP': iperfArgs += '-u ' bwArgs = '-b ' + udpBw + ' ' server.sendCmd( iperfArgs + '-s' ) serverip = server.IP() if l4Type == 'TCP': if not waitListening( client, serverip, port ): raise Exception( 'Could not connect to iperf on port %d' % port ) cliout = client.cmd( iperfArgs + '-t %d -c ' % seconds + server.IP() + ' ' + bwArgs ) cvals = self._iperfVals( cliout, serverip ) debug( 'iperf client output:', cliout, cvals ) serverout = '' # Wait for output from the client session while True: serverout += server.monitor( timeoutms=5000 ) svals = self._iperfVals( serverout, serverip ) # Check for the client's source/output port if ( svals and cvals[ 'sport' ] == svals[ 'sport' ] and int( svals[ 'rate' ] ) > 0 ): break debug( 'iperf server output:', serverout, svals ) server.sendInt() serverout += server.waitOutput() result = [ fmtBps( svals[ 'rate'], fmt ), fmtBps( cvals[ 'rate' ], fmt ) ] if l4Type == 'UDP': result.insert( 0, udpBw ) output( '*** Results: %s\n' % result ) return result def runCpuLimitTest( self, cpu, duration=5 ): """run CPU limit test with 'while true' processes. cpu: desired CPU fraction of each host duration: test duration in seconds (integer) returns a single list of measured CPU fractions as floats. """ pct = cpu * 100 info( '*** Testing CPU %.0f%% bandwidth limit\n' % pct ) hosts = self.hosts cores = int( quietRun( 'nproc' ) ) # number of processes to run a while loop on per host num_procs = int( ceil( cores * cpu ) ) pids = {} for h in hosts: pids[ h ] = [] for _core in range( num_procs ): h.cmd( 'while true; do a=1; done &' ) pids[ h ].append( h.cmd( 'echo $!' ).strip() ) outputs = {} time = {} # get the initial cpu time for each host for host in hosts: outputs[ host ] = [] with open( '/sys/fs/cgroup/cpuacct/%s/cpuacct.usage' % host, 'r' ) as f: time[ host ] = float( f.read() ) for _ in range( duration ): sleep( 1 ) for host in hosts: with open( '/sys/fs/cgroup/cpuacct/%s/cpuacct.usage' % host, 'r' ) as f: readTime = float( f.read() ) outputs[ host ].append( ( ( readTime - time[ host ] ) / 1000000000 ) / cores * 100 ) time[ host ] = readTime for h, pids in pids.items(): for pid in pids: h.cmd( 'kill -9 %s' % pid ) cpu_fractions = [] for _host, outputs in outputs.items(): for pct in outputs: cpu_fractions.append( pct ) output( '*** Results: %s\n' % cpu_fractions ) return cpu_fractions # BL: I think this can be rewritten now that we have # a real link class. def configLinkStatus( self, src, dst, status ): """Change status of src <-> dst links. src: node name dst: node name status: string {up, down}""" if src not in self.nameToNode: error( 'src not in network: %s\n' % src ) elif dst not in self.nameToNode: error( 'dst not in network: %s\n' % dst ) else: src = self.nameToNode[ src ] dst = self.nameToNode[ dst ] connections = src.connectionsTo( dst ) if len( connections ) == 0: error( 'src and dst not connected: %s %s\n' % ( src, dst) ) for srcIntf, dstIntf in connections: result = srcIntf.ifconfig( status ) if result: error( 'link src status change failed: %s\n' % result ) result = dstIntf.ifconfig( status ) if result: error( 'link dst status change failed: %s\n' % result ) def interact( self ): "Start network and run our simple CLI." self.start() result = CLI( self ) self.stop() return result inited = False @classmethod def init( cls ): "Initialize Mininet" if cls.inited: return ensureRoot() fixLimits() cls.inited = True class MininetWithControlNet( Mininet ): """Control network support: Create an explicit control network. Currently this is only used/usable with the user datapath. Notes: 1. If the controller and switches are in the same (e.g. root) namespace, they can just use the loopback connection. 2. If we can get unix domain sockets to work, we can use them instead of an explicit control network. 3. Instead of routing, we could bridge or use 'in-band' control. 4. Even if we dispense with this in general, it could still be useful for people who wish to simulate a separate control network (since real networks may need one!) 5. Basically nobody ever used this code, so it has been moved into its own class. 6. Ultimately we may wish to extend this to allow us to create a control network which every node's control interface is attached to.""" def configureControlNetwork( self ): "Configure control network." self.configureRoutedControlNetwork() # We still need to figure out the right way to pass # in the control network location. def configureRoutedControlNetwork( self, ip='192.168.123.1', prefixLen=16 ): """Configure a routed control network on controller and switches. For use with the user datapath only right now.""" controller = self.controllers[ 0 ] info( controller.name + ' <->' ) cip = ip snum = ipParse( ip ) for switch in self.switches: info( ' ' + switch.name ) link = self.link( switch, controller, port1=0 ) sintf, cintf = link.intf1, link.intf2 switch.controlIntf = sintf snum += 1 while snum & 0xff in [ 0, 255 ]: snum += 1 sip = ipStr( snum ) cintf.setIP( cip, prefixLen ) sintf.setIP( sip, prefixLen ) controller.setHostRoute( sip, cintf ) switch.setHostRoute( cip, sintf ) info( '\n' ) info( '*** Testing control network\n' ) while not cintf.isUp(): info( '*** Waiting for', cintf, 'to come up\n' ) sleep( 1 ) for switch in self.switches: while not sintf.isUp(): info( '*** Waiting for', sintf, 'to come up\n' ) sleep( 1 ) if self.ping( hosts=[ switch, controller ] ) != 0: error( '*** Error: control network test failed\n' ) exit( 1 ) info( '\n' )