"""
vmm(4)-based nodes for using VMs as hosts and network nodes.

A VM node is not in an rdomain (inNamespace=False), since it is
already self-contained. It understands how to use vmctl to
configure, start, and stop itself.

There are three link configurations:

- VM to VM (either between hosts or network nodes): These are links
made of two tap(4) interfaces "patched" together with a bridge(4)
device. The tap interfaces aren't assigned IP addresses.

- VM to host (between a regular Host and VM host/switch): The tap
interface is added to a Host's rdomain. It therefore takes on the
Host's IP address.

- VM to switch (regular IfSwitch or Bridge4 node): The tap
interface is added to the switch as a port. It isn't assigned an
IP address.

We don't rely on vm.conf to configure switches or IPs, since we
create and manage nodes/assign IPs ourselves and it's easy to step
on feet.
"""
from mininet.log import debug, info, warn
from mininet.util import quietRun

from mininet.openbsd.node import Node

class VmmNode( Node ):
    """
    There are several questions to deal with:
        - which tap(4) interface(s) do a VM get? There is a chance that it will
          run out. In which case, MAKEDEV(8) needs to be run. But, we don't
	  specify an interface. Unless we do in some vm.conf(5). 
    """

    maxWait = 45    # max retries/seconds(ish) 

    # ipBlock: the default IP range VMs are assigned, may be changed by vm.conf(5) 
    def __init__( self, name, disk, ipBlock='100.64', mem=64, inNamespace=False,  **params ):
        self.disk = disk
        self.ipBlock = ipBlock
        self.mem = mem
        Node.__init__( self, name, inNamespace=inNamespace, **params )


    def start( self ):
        """ 
        boot the VM. We need the number of interfaces, disk image, memory, and
	wait until we can see the login?
	"""
        # from vmctl(4)
        vccmd = "vmctl start %s -Lm %d -i %d -d %s" % \
                ( self.name, self.mem, len( self.ports ), self.disk )
        res = self.cmd( vccmd )

        if 'started vm' in res:
            # 'vmctl: started vm n successfully, tty /dev/foo'
            l = res.split()
            self.vmid = int( l[ 3 ] )
            self.console = l[ -1 ]
        else:
            warn( 'result was: %s, VM likely not started, cleaning up\n' % res )
            self.stop()
            return

        self.waitStart()


    def getTap( self ):
        """
        Parse output to find the host-side tap interface.
        """
        res = self.cmd( 'ifconfig tap | grep -B4 "description:.*%s"' % self.name )
        for line in res.split( '\n' ):
            l = line.strip().split()
            if len( l ) == 0:
                continue
            if l[ 0 ].startswith( 'tap ' ):
                return l[ 0 ]
        return None


    def waitStart( self ):
        """
        Wait for the VM to come up enough to respond to pings.
        We take advantage of the local interface and its IP allocation pattern
        to find the VM's address.
        """
        # The tap(4) instance attached to a VM has the VM's name in its
        # description field, and has a host part ending in '2'. The VM is '3'. 
        self.tapdev = self.getTap()
        if not self.tapdev:
            import time
            retry = 1
            while not self.tapdev:
                time.sleep( 0.25 )
                if retry == 3:
                    warn( 'could not find local interface, giving up' )
                    self.stop()
                    return
                self.tapdev = self.getTap()
                retry += 1   

        self.localIP = '%s.%d.3' % ( self.ipBlock, self.vmid )
        debug( '%s at %s, IP=%s\n' % ( self.name, self.tapdev, self.localIP ) )

        info( 'Waiting for %s to start\n' )
        retry = 1 
        cmd = 'ping -c1 -w1 ' + self.localIP
        res = self.cmd( cmd )
        while 'bytes from ' not in res:
            # do we want to retry the VM itself?
            if retry == VmmNode.maxWait:
                warn( 'exceeded wait-time for VM boot, giving up' )
                self.stop()
                return 
            info( '.' )
            res = self.cmd( cmd )
            retry += 1
        info('OK\n')


    def stop( self ):
        """
        stop the VM. Do we want to wait until it's dead for sure?
        """
        res = self.cmd( 'vmctl stop %d' % self.vmid )
        debug( res ) 


    @classmethod
    def setup( cls ):
        """ check if vmd(8) is running""" 
        p = self.cmd( 'pgrep -f vmd' )
        if not p:
            quietRun( 'vmd -v' )
