"""
Node: rdomain(4) based node. This is somewhat more similar to Linux's network
namespace moreso than a jail since it creates a separate network address space
only.

Mininet 'hosts' are created by running shells within rdomains. Links are made of
pair(4)s patched together.

This is a collection of helpers that call the right commands to manipulate these
components.
"""
import signal
import re
from os import killpg

from subprocess import PIPE, Popen
from mininet.basenode import BaseNode
from mininet.log import debug, info, warn
from mininet.util import quietRun

from mininet.openbsd.util import moveIntf
from mininet.openbsd.intf import Intf

class Node( BaseNode ):
    """A virtual network node that manipulates and tracks rdomains. Because of
       the property of rdomains, an OpenBSD node will always come with at least
       one pair interface if inNamespace=True."""

    index=1     # rdomain ID, can only go to 255
    builtins='. : [ alias bg break builtin cd command continue echo eval exec' \
             ' exit export false fc fg getopts jobs kill let print pwd read' \
             ' readonly return set shift suspend test times trap true typeset' \
             ' ulimit umask unalias unset wait whence'

    def __init__( self, name, inNamespace=True, **params ):
        BaseNode.__init__( self, name, inNamespace, **params )
        # No renaming, supply map of assigned interface names to real names
        self.portNames = {}

    def getShell( self, master, slave, mnopts=None ):
        """
        Starts a shell used by the node to run commands. If inNamespace=True,
        a pair interface is created, assigned to an rdomain, and a shell is
        exec'd in the rdomain.
        """
        execcmd = [ 'mnexec' ]
        opts = '-cd' if mnopts is None else mnopts

        if self.inNamespace:
            # create the pair tied to an rdomain
            self.pair, self.rdid = 'pair%d' % Intf.next(), Node.index
            Node.index += 1
            if Node.index == 256:
                error( 'Exceeded supported number of hosts (255)' )
                exit( 1 )
            rcmd = [ 'ifconfig', self.pair, 'create', 'description',
                     '"%s"' % self.name, 'rdomain', '%d' % self.rdid ]
            Popen( rcmd, stdout=PIPE )
            execcmd = [ 'route', '-T%d' % self.rdid, 'exec' ] + execcmd
        else:
            self.pair = None
            self.rdid = None

        # -s: pass $* to shell, and make process easy to find in ps. The prompt
        # is set to sentinel chr( 127 ).
        cmd = execcmd + [ opts, 'env', 'PS1=' + chr( 127 ), '/bin/ksh',
                          '-is', 'mininet:' + self.name ]
        return Popen( cmd, stdin=slave, stdout=slave, stderr=slave,
                      close_fds=False )

    def isShellBuiltin( self, cmd ):
        "Return True if cmd is a builtin."
        space = cmd.find( ' ' )
        if space > 0:
            cmd = cmd[ :space]
        return cmd in Node.builtins

    def mountPrivateDirs( self ):
        "mount private directories"
        # **Not applicable until further notice**
        pass

    def unmountPrivateDirs( self ):
        "mount private directories -  overridden"
        # **Not applicable until further notice**
        pass

    def terminate( self ):
        """ Cleanup when node is killed.  """
        #self.unmountPrivateDirs()
        if self.rdid:
            Popen( [ 'ifconfig', self.pair, 'destroy' ] )
        # For 6.1, the following will work, but since the rtables stick around
        # there is probably no harm in leaving them versus trying what won't work.
        #    # cleanup loopbacks that are left
        #    lo = 'lo%s' % self.rdid
        #    Popen( [ 'ifconfig', lo, 'rdomain', '0', 'destroy' ] )
        if self.shell:
            if self.shell.poll() is None:
                killpg( self.shell.pid, signal.SIGHUP )
        self.cleanup()

    def popen( self, *args, **kwargs ):
        """Return a Popen() object in our namespace
           args: Popen() args, single list, or string
           kwargs: Popen() keyword args"""
        defaults = { 'stdout': PIPE, 'stderr': PIPE,
                     'mncmd':
                     [ 'route', '-T%d' % self.rdid, 'exec' ] if self.rdid else
                     [ 'mnexec', '-d' ] }
        defaults.update( kwargs )
        if len( args ) == 1:
            if isinstance( args[ 0 ], list ):
                # popen([cmd, arg1, arg2...])
                cmd = args[ 0 ]
            elif isinstance( args[ 0 ], basestring ):
                # popen("cmd arg1 arg2...")
                cmd = args[ 0 ].split()
            else:
                raise Exception( 'popen() requires a string or list' )
        elif len( args ) > 0:
            # popen( cmd, arg1, arg2... )
            cmd = list( args )
        # Attach to our namespace  using mnexec -a
        cmd = defaults.pop( 'mncmd' ) + cmd
        # Shell requires a string, not a list!
        if defaults.get( 'shell', False ):
            cmd = ' '.join( cmd )
        popen = self._popen( cmd, **defaults )
        return popen

    def sendInt( self, intr=chr( 3 ) ):
        "Interrupt running command."
        quietRun( 'kill -2 %s' % self.lastPid )

    def setHostRoute( self, ip, intf ):
        """Add route to host.
           ip: IP address as dotted decimal
           intf: string, interface name
           intfs: interface map of names to Intf"""
        # add stronger checks for interface lookup
        cmd = 'route -T%s add -host %s %s'
        quietRun( cmd % ( self.rdid, ip, self.intfs( intf ).IP() ) )

    def setDefaultRoute( self, intf=None ):
        """Set the default route to go through intf.
           intf: Intf or {dev <intfname> via <gw-ip> ...}"""
        # Note setParam won't call us if intf is none
        if isinstance( intf, basestring ):
            argv = intf.split(' ')
            if 'via' not in argv[ 0 ]:
                warn( '%s: setDefaultRoute takes a port name but we got: %s\n' %
                      ( self.name, intf ) )
                return
            params = argv[ -1 ]
        else:
            params = intf.IP()
        quietRun( 'route -T%d change default %s' % ( self.rdid, params ) )


    def addIntf( self, intf, port=None, moveIntfFn=moveIntf ):
        self.portNames[ intf.name ] = intf.realname
        super( Node, self ).addIntf( intf, port, moveIntfFn )
