#!/usr/bin/env python

"""
Mininet runner
author: Brandon Heller (brandonh@stanford.edu)

To see options:
  sudo mn -h

Example to pull custom params (topo, switch, etc.) from a file:
  sudo mn --custom ~/mininet/custom/custom_example.py
"""

from optparse import OptionParser
import os
import sys
import time

# Fix setuptools' evil madness, and open up (more?) security holes
if 'PYTHONPATH' in os.environ:
    sys.path = os.environ[ 'PYTHONPATH' ].split( ':' ) + sys.path

from mininet.clean import cleanup
from mininet.cli import CLI
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,
                           RYU, NOX, RemoteController, findController, DefaultController,
                           UserSwitch, OVSSwitch, OVSBridge,
                           OVSLegacyKernelSwitch, IVSSwitch )
from mininet.nodelib import LinuxBridge
from mininet.link import Link, TCLink
from mininet.topo import SingleSwitchTopo, LinearTopo, SingleSwitchReversedTopo
from mininet.topolib import TreeTopo, TorusTopo
from mininet.util import customConstructor, splitArgs
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 ClusterCLI

PLACEMENT = { 'block': SwitchBinPlacer, 'random': RandomPlacer }

# built in topologies, created only when run
TOPODEF = 'minimal'
TOPOS = { 'minimal': lambda: SingleSwitchTopo( k=2 ),
          'linear': LinearTopo,
          'reversed': SingleSwitchReversedTopo,
          'single': SingleSwitchTopo,
          'tree': TreeTopo,
          'torus': TorusTopo }

SWITCHDEF = 'default'
SWITCHES = { 'user': UserSwitch,
             'ovs':  OVSSwitch,
             'ovsbr' : OVSBridge,
             # Keep ovsk for compatibility with 2.0
             'ovsk': OVSSwitch,
             'ovsl': OVSLegacyKernelSwitch,
             'ivs': IVSSwitch,
             'lxbr': LinuxBridge,
             'default': OVSSwitch }

HOSTDEF = 'proc'
HOSTS = { 'proc': Host,
          'rt': partial( CPULimitedHost, sched='rt' ),
          'cfs': partial( CPULimitedHost, sched='cfs' ) }

CONTROLLERDEF = 'default'
CONTROLLERS = { 'ref': Controller,
                'ovsc': OVSController,
                'nox': NOX,
                'remote': RemoteController,
                'ryu': RYU,
                'default': DefaultController,  # Note: replaced below
                'none': lambda name: None }

LINKDEF = 'default'
LINKS = { 'default': Link,
          'tc': TCLink }


# optional tests to run
TESTS = [ 'cli', 'build', 'pingall', 'pingpair', 'iperf', 'all', 'iperfudp',
          'none' ]

ALTSPELLING = { 'pingall': 'pingAll',
                'pingpair': 'pingPair',
                'iperfudp': 'iperfUdp',
                'iperfUDP': 'iperfUdp' }


def addDictOption( opts, choicesDict, default, name, helpStr=None ):
    """Convenience function to add choices dicts to OptionParser.
       opts: OptionParser instance
       choicesDict: dictionary of valid choices, must include default
       default: default choice key
       name: long option name
       help: string"""
    if default not in choicesDict:
        raise Exception( 'Invalid  default %s for choices dict: %s' %
                         ( default, name ) )
    if not helpStr:
        helpStr = ( '|'.join( sorted( choicesDict.keys() ) ) +
                    '[,param=value...]' )
    opts.add_option( '--' + name,
                     type='string',
                     default = default,
                     help = helpStr )


def version( *_args ):
    "Print Mininet version and exit"
    print "%s" % VERSION
    exit()


class MininetRunner( object ):
    "Build, setup, and run Mininet."

    def __init__( self ):
        "Init."
        self.options = None
        self.args = None  # May be used someday for more CLI scripts
        self.validate = None

        self.parseArgs()
        self.setup()
        self.begin()

    def custom( self, option, opt_str, value, parser ):
        """Parse custom file and add params.
           option: option e.g. --custom
           opt_str: option string e.g. --custom
           value: the value the follows the option
           parser: option parser instance"""
        files = []
        if os.path.isfile( value ):
            # Accept any single file (including those with commas)
            files.append( value )
        else:
            # Accept a comma-separated list of filenames
            files += value.split(',')
        
        for fileName in files:
            customs = {}
            if os.path.isfile( fileName ):
                execfile( fileName, customs, customs )
                for name, val in customs.iteritems():
                    self.setCustom( name, val )
            else:
                raise Exception( 'could not find custom file: %s' % fileName )

    def setCustom( self, name, value ):
        "Set custom parameters for MininetRunner."
        if name in ( 'topos', 'switches', 'hosts', 'controllers' ):
            # Update dictionaries
            param = name.upper()
            globals()[ param ].update( value )
        elif name == 'validate':
            # Add custom validate function
            self.validate = value
        else:
            # Add or modify global variable or class
            globals()[ name ] = value

    def setNat( self, option, opt_str, value, parser ):
        parser.values.nat = True
        if parser.rargs and parser.rargs[ 0 ][ 0 ] != '-': #first arg, first char != '-'
            value = parser.rargs.pop( 0 )
            _, args, kwargs = splitArgs( opt_str + ',' + value )
            parser.values.nat_args = args
            parser.values.nat_kwargs = kwargs
        else:
            parser.values.nat_args = []
            parser.values.nat_kwargs = {}

    def parseArgs( self ):
        """Parse command-line args and return options object.
           returns: opts parse options dict"""

        desc = ( "The %prog utility creates Mininet network from the\n"
                 "command line. It can create parametrized topologies,\n"
                 "invoke the Mininet CLI, and run tests." )

        usage = ( '%prog [options]\n'
                  '(type %prog -h for details)' )

        opts = OptionParser( description=desc, usage=usage )
        addDictOption( opts, SWITCHES, SWITCHDEF, 'switch' )
        addDictOption( opts, HOSTS, HOSTDEF, 'host' )
        addDictOption( opts, CONTROLLERS, CONTROLLERDEF, 'controller' )
        addDictOption( opts, LINKS, LINKDEF, 'link' )
        addDictOption( opts, TOPOS, TOPODEF, 'topo' )

        opts.add_option( '--clean', '-c', action='store_true',
                         default=False, help='clean and exit' )
        opts.add_option( '--custom', action='callback',
                         callback=self.custom,
                         type='string', help='read custom classes or params from .py file(s)' )
        opts.add_option( '--test', type='choice', choices=TESTS,
                         default=TESTS[ 0 ],
                         help='|'.join( TESTS ) )
        opts.add_option( '--xterms', '-x', action='store_true',
                         default=False, help='spawn xterms for each node' )
        opts.add_option( '--ipbase', '-i', type='string', default='10.0.0.0/8',
                         help='base IP address for hosts' )
        opts.add_option( '--mac', action='store_true',
                         default=False, help='automatically set host MACs' )
        opts.add_option( '--arp', action='store_true',
                         default=False, help='set all-pairs ARP entries' )
        opts.add_option( '--verbosity', '-v', type='choice',
                         choices=LEVELS.keys(), default = 'info',
                         help = '|'.join( LEVELS.keys() )  )
        opts.add_option( '--innamespace', action='store_true',
                         default=False, help='sw and ctrl in namespace?' )
        opts.add_option( '--listenport', type='int', default=6634,
                         help='base port for passive switch listening' )
        opts.add_option( '--nolistenport', action='store_true',
                         default=False, help="don't use passive listening " +
                         "port")
        opts.add_option( '--pre', type='string', default=None,
                         help='CLI script to run before tests' )
        opts.add_option( '--post', type='string', default=None,
                         help='CLI script to run after tests' )
        opts.add_option( '--pin', action='store_true',
                         default=False, help="pin hosts to CPU cores "
                         "(requires --host cfs or --host rt)" )
        opts.add_option( '--nat', action='callback', callback=self.setNat,
                         help="adds a NAT to the topology that connects Mininet hosts"
                         " to the physical network."
                         " Warning: This may route any traffic on the machine that uses Mininet's"
                         " IP subnet into the Mininet network. If you need to change"
                         " Mininet's IP subnet, see the --ipbase option." )
        opts.add_option( '--version', action='callback', callback=version,
                         help='prints the version and exits' )
        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()

        # We don't accept extra arguments after the options
        if self.args:
            opts.print_help()
            exit()

    def setup( self ):
        "Setup and validate environment."

        # set logging verbosity
        if LEVELS[self.options.verbosity] > LEVELS['output']:
            print ( '*** WARNING: selected verbosity level (%s) will hide CLI '
                    'output!\n'
                    'Please restart Mininet with -v [debug, info, output].'
                    % self.options.verbosity )
        lg.setLogLevel( self.options.verbosity )

    def begin( self ):
        "Create and run mininet."

        if self.options.clean:
            cleanup()
            exit()

        start = time.time()

        if self.options.controller == 'default':
            # Update default based on available controllers
            CONTROLLERS[ 'default' ] = findController()
            if CONTROLLERS[ 'default' ] is None:
                if self.options.switch == 'default':
                    # Fall back to OVS Bridge, which does not use an OF controller
                    info( '*** No default OpenFlow controller found for default switch!\n' )
                    info( '*** Falling back to OVS Bridge\n' )
                    self.options.switch = 'ovsbr'
                    self.options.controller = 'none'
                elif self.options.switch in ( 'ovsbr', 'lxbr' ):
                    self.options.controller = 'none'
                else:
                    raise Exception( "Could not find a default controller for switch %s" %
                                     self.options.switch )
        
        topo = buildTopo( TOPOS, self.options.topo )
        switch = customConstructor( SWITCHES, self.options.switch )
        host = customConstructor( HOSTS, self.options.host )
        controller = customConstructor( CONTROLLERS, self.options.controller )
        link = customConstructor( LINKS, self.options.link )

        if self.validate:
            self.validate( self.options )

        ipBase = self.options.ipbase
        xterms = self.options.xterms
        mac = self.options.mac
        arp = self.options.arp
        pin = self.options.pin
        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
        cli = ClusterCLI if cluster else CLI
        if cluster:
            warn( '*** WARNING: Experimental cluster mode!\n'
              '*** Using RemoteHost, RemoteOVSSwitch, RemoteLink\n' )
            host, switch, link = RemoteHost, RemoteOVSSwitch, RemoteLink
            Net = partial( MininetCluster, servers=cluster.split( ',' ),
                           placement=PLACEMENT[ self.options.placement ] )

        mn = Net( topo=topo,
                  switch=switch, host=host, controller=controller,
                  link=link,
                  ipBase=ipBase,
                  inNamespace=inNamespace,
                  xterms=xterms, autoSetMacs=mac,
                  autoStaticArp=arp, autoPinCpus=pin,
                  listenPort=listenPort )

        if self.options.nat:
            nat = mn.addNAT( *self.options.nat_args, **self.options.nat_kwargs )
            nat.configDefault()

        if self.options.pre:
            cli( mn, script=self.options.pre )

        test = self.options.test
        test = ALTSPELLING.get( test, test )

        mn.start()

        if test == 'none':
            pass
        elif test == 'all':
            mn.waitConnected()
            mn.start()
            mn.ping()
            mn.iperf()
        elif test == 'cli':
            cli( mn )
        elif test != 'build':
            mn.waitConnected()
            getattr( mn, test )()

        if self.options.post:
            cli( mn, script=self.options.post )

        mn.stop()

        elapsed = float( time.time() - start )
        info( 'completed in %0.3f seconds\n' % elapsed )


if __name__ == "__main__":
    try:
        MininetRunner()
    except KeyboardInterrupt:
        info( "\n\nKeyboard Interrupt. Shutting down and cleaning up...\n\n")
        cleanup()
    except Exception:
        # Print exception
        type_, val_, trace_ = sys.exc_info()
        errorMsg = ( "-"*80 + "\n" +
                     "Caught exception. Cleaning up...\n\n" +
                     "%s: %s\n" % ( type_.__name__, val_ ) +
                     "-"*80 + "\n" )
        error( errorMsg )
        # Print stack trace to debug log
        import traceback
        stackTrace = traceback.format_exc()
        debug( stackTrace + "\n" )
        cleanup()