#
# CORE
# Copyright (c)2011-2012 the Boeing Company.
# See the LICENSE file included in this distribution.
#
# author: Jeff Ahrenholz <jeffrey.m.ahrenholz@boeing.com>
#
''' PhysicalNode class for including real systems in the emulated network.
'''
import os, threading, subprocess

from core.misc.ipaddr import *
from core.misc.utils import *
from core.constants import *
from core.api import coreapi
from core.coreobj import PyCoreNode, PyCoreNetIf
from core.emane.nodes import EmaneNode
if os.uname()[0] == "Linux":
    from core.netns.vnet import LxBrNet
    from core.netns.vif import GreTap
elif os.uname()[0] == "FreeBSD":
    from core.bsd.vnet import NetgraphNet


class PhysicalNode(PyCoreNode):
    def __init__(self, session, objid = None, name = None,
                 nodedir = None, verbose = False, start = True):
        PyCoreNode.__init__(self, session, objid, name, verbose=verbose,
                            start=start)
        self.nodedir = nodedir
        self.up = start
        self.lock = threading.RLock()
        self._mounts = []
        if start:
            self.startup()
        
    def boot(self):
        self.session.services.bootnodeservices(self)
        
    def validate(self):
        self.session.services.validatenodeservices(self)

    def startup(self):
        self.lock.acquire()
        try:
            self.makenodedir()
            #self.privatedir("/var/run")
            #self.privatedir("/var/log")
        except OSError, e:
            self.exception(coreapi.CORE_EXCP_LEVEL_ERROR,
                "PhysicalNode.startup()", e)
        finally:
            self.lock.release()

    def shutdown(self):
        if not self.up:
            return
        self.lock.acquire()
        while self._mounts:
            source, target = self._mounts.pop(-1)
            self.umount(target)
        for netif in self.netifs():
            netif.shutdown()
        self.rmnodedir()
        self.lock.release()


    def termcmdstring(self, sh = "/bin/sh"):
        ''' The broker will add the appropriate SSH command to open a terminal
        on this physical node.
        '''
        return sh
        
    def cmd(self, args, wait = True):
        ''' run a command on the physical node
        '''
        os.chdir(self.nodedir)
        try:
            if wait:
                # os.spawnlp(os.P_WAIT, args)
                subprocess.call(args)
            else:
                # os.spawnlp(os.P_NOWAIT, args)
                subprocess.Popen(args)
        except CalledProcessError, e:
            self.warn("cmd exited with status %s: %s" % (e, str(args)))
        
    def cmdresult(self, args):
        ''' run a command on the physical node and get the result
        '''
        os.chdir(self.nodedir)
        # in Python 2.7 we can use subprocess.check_output() here
        tmp = subprocess.Popen(args, stdin = subprocess.PIPE,
                               stdout = subprocess.PIPE,
                               stderr = subprocess.PIPE)
        result = tmp.stdout.read()
        result += tmp.stderr.read()
        tmp.stdin.close()
        tmp.stdout.close()
        tmp.stderr.close()
        status = tmp.wait()
        return (status, result)
        
    def shcmd(self, cmdstr, sh = "/bin/sh"):
        return self.cmd([sh, "-c", cmdstr])

    def sethwaddr(self, ifindex, addr):
        ''' same as SimpleLxcNode.sethwaddr()
        '''
        self._netif[ifindex].sethwaddr(addr)
        ifname = self.ifname(ifindex)
        if self.up:
            (status, result) = self.cmdresult([IP_BIN, "link", "set", "dev",
                                              ifname, "address", str(addr)])
            if status:
                self.exception(coreapi.CORE_EXCP_LEVEL_ERROR,
                    "PhysicalNode.sethwaddr()",
                    "error setting MAC address %s" % str(addr))
                    
    def addaddr(self, ifindex, addr):
        ''' same as SimpleLxcNode.addaddr()
        '''
        if self.up:
            self.cmd([IP_BIN, "addr", "add", str(addr),
                  "dev", self.ifname(ifindex)])
        self._netif[ifindex].addaddr(addr)

    def deladdr(self, ifindex, addr):
        ''' same as SimpleLxcNode.deladdr()
        '''
        try:
            self._netif[ifindex].deladdr(addr)
        except ValueError:
            self.warn("trying to delete unknown address: %s" % addr)
        if self.up:
            self.cmd([IP_BIN, "addr", "del", str(addr),
                  "dev", self.ifname(ifindex)])

    def adoptnetif(self, netif, ifindex, hwaddr, addrlist):
        ''' The broker builds a GreTap tunnel device to this physical node.
        When a link message is received linking this node to another part of
        the emulation, no new interface is created; instead, adopt the
        GreTap netif as the node interface.
        '''
        netif.name = "gt%d" % ifindex
        netif.node = self
        self.addnetif(netif, ifindex)
        # use a more reasonable name, e.g. "gt0" instead of "gt.56286.150"
        if self.up:
            self.cmd([IP_BIN, "link", "set", "dev", netif.localname, "down"])
            self.cmd([IP_BIN, "link", "set", netif.localname, "name", netif.name])
        netif.localname = netif.name
        if hwaddr:
            self.sethwaddr(ifindex, hwaddr)
        for addr in maketuple(addrlist):
            self.addaddr(ifindex, addr)
        if self.up:
            self.cmd([IP_BIN, "link", "set", "dev", netif.localname, "up"])
            
    def linkconfig(self, netif, bw = None, delay = None,
                   loss = None, duplicate = None, jitter = None, netif2 = None):
        ''' Apply tc queing disciplines using LxBrNet.linkconfig()
        '''
        if os.uname()[0] == "Linux":
            netcls = LxBrNet
        elif os.uname()[0] == "FreeBSD":
            netcls = NetgraphNet
        else:
            raise NotImplementedError, "unsupported platform"
        # borrow the tc qdisc commands from LxBrNet.linkconfig()
        tmp = netcls(session=self.session, start=False)
        tmp.up = True
        tmp.linkconfig(netif, bw=bw, delay=delay, loss=loss,
                       duplicate=duplicate, jitter=jitter, netif2=netif2)
        del tmp

    def newifindex(self):
        self.lock.acquire()
        try:
            while self.ifindex in self._netif:
                self.ifindex += 1
            ifindex = self.ifindex
            self.ifindex += 1
            return ifindex
        finally:
            self.lock.release()

    def newnetif(self, net = None, addrlist = [], hwaddr = None,
                 ifindex = None, ifname = None):
        if self.up and net is None:
            raise NotImplementedError
        if ifindex is None:
            ifindex = self.newifindex()

        if self.up:
            # this is reached when this node is linked to a network node
            # tunnel to net not built yet, so build it now and adopt it
            gt = self.session.broker.addnettunnel(net.objid)
            if gt is None or len(gt) != 1:
                self.session.warn("Error building tunnel from PhysicalNode."
                                  "newnetif()")
            gt = gt[0]
            net.detach(gt)
            self.adoptnetif(gt, ifindex, hwaddr, addrlist)
            return ifindex
            
        # this is reached when configuring services (self.up=False)
        if ifname is None:
            ifname = "gt%d" % ifindex
        netif = GreTap(node = self, name = ifname, session = self.session,
                       start = False)
        self.adoptnetif(netif, ifindex, hwaddr, addrlist)
        return ifindex
        
        
    def privatedir(self, path):
        if path[0] != "/":
            raise ValueError, "path not fully qualified: " + path
        hostpath = os.path.join(self.nodedir, path[1:].replace("/", "."))
        try:
            os.mkdir(hostpath)
        except OSError:
            pass
        except Exception, e:
            raise Exception, e
        self.mount(hostpath, path)

    def mount(self, source, target):
        source = os.path.abspath(source)
        self.info("mounting %s at %s" % (source, target))
        try:
            os.makedirs(target)
        except OSError:
            pass
        try:
            self.cmd([MOUNT_BIN, "--bind", source, target])
            self._mounts.append((source, target))
        except:
            self.warn("mounting failed for %s at %s" % (source, target))

    def umount(self, target):
        self.info("unmounting '%s'" % target)
        try:
            self.cmd([UMOUNT_BIN, "-l", target])
        except:
            self.warn("unmounting failed for %s" % target)

    def opennodefile(self, filename, mode = "w"):
        dirname, basename = os.path.split(filename)
        if not basename:
            raise ValueError, "no basename for filename: " + filename
        if dirname and dirname[0] == "/":
            dirname = dirname[1:]
        dirname = dirname.replace("/", ".")
        dirname = os.path.join(self.nodedir, dirname)
        if not os.path.isdir(dirname):
            os.makedirs(dirname, mode = 0755)
        hostfilename = os.path.join(dirname, basename)
        return open(hostfilename, mode)

    def nodefile(self, filename, contents, mode = 0644):
        f = self.opennodefile(filename, "w")
        f.write(contents)
        os.chmod(f.name, mode)
        f.close()
        self.info("created nodefile: '%s'; mode: 0%o" % (f.name, mode))