#
# CORE
# Copyright (c)2010-2012 the Boeing Company.
# See the LICENSE file included in this distribution.
#
# authors: core-dev@pf.itd.nrl.navy.mil
#

"""
vnet.py: NetgraphNet and NetgraphPipeNet classes that implement virtual networks
using the FreeBSD Netgraph subsystem.
"""

from core.bsd.netgraph import connectngnodes
from core.bsd.netgraph import createngnode
from core.bsd.netgraph import destroyngnode
from core.bsd.netgraph import ngmessage
from core.coreobj import PyCoreNet


class NetgraphNet(PyCoreNet):
    ngtype = None
    nghooks = ()

    def __init__(self, session, objid=None, name=None, verbose=False,
                 start=True, policy=None):
        PyCoreNet.__init__(self, session, objid, name)
        if name is None:
            name = str(self.objid)
        if policy is not None:
            self.policy = policy
        self.name = name
        self.ngname = "n_%s_%s" % (str(self.objid), self.session.sessionid)
        self.ngid = None
        self.verbose = verbose
        self._netif = {}
        self._linked = {}
        self.up = False
        if start:
            self.startup()

    def startup(self):
        tmp, self.ngid = createngnode(type=self.ngtype, hookstr=self.nghooks, name=self.ngname)
        self.up = True

    def shutdown(self):
        if not self.up:
            return
        self.up = False
        while self._netif:
            k, netif = self._netif.popitem()
            if netif.pipe:
                pipe = netif.pipe
                netif.pipe = None
                pipe.shutdown()
            else:
                netif.shutdown()
        self._netif.clear()
        self._linked.clear()
        del self.session
        destroyngnode(self.ngname)

    def attach(self, netif):
        """ Attach an interface to this netgraph node. Create a pipe between
            the interface and the hub/switch/wlan node.
            (Note that the PtpNet subclass overrides this method.)
        """
        if self.up:
            pipe = self.session.addobj(cls=NetgraphPipeNet, verbose=self.verbose, start=True)
            pipe.attach(netif)
            hook = "link%d" % len(self._netif)
            pipe.attachnet(self, hook)
        PyCoreNet.attach(self, netif)

    def detach(self, netif):
        if self.up:
            pass
        PyCoreNet.detach(self, netif)

    def linked(self, netif1, netif2):
        # check if the network interfaces are attached to this network
        if self._netif[netif1] != netif1:
            raise ValueError("inconsistency for netif %s" % netif1.name)
        if self._netif[netif2] != netif2:
            raise ValueError("inconsistency for netif %s" % netif2.name)

        try:
            linked = self._linked[netif1][netif2]
        except KeyError:
            linked = False
            self._linked[netif1][netif2] = linked

        return linked

    def unlink(self, netif1, netif2):
        if not self.linked(netif1, netif2):
            return
        msg = ["unlink", "{", "node1=0x%s" % netif1.pipe.ngid]
        msg += ["node2=0x%s" % netif2.pipe.ngid, "}"]
        ngmessage(self.ngname, msg)
        self._linked[netif1][netif2] = False

    def link(self, netif1, netif2):
        if self.linked(netif1, netif2):
            return
        msg = ["link", "{", "node1=0x%s" % netif1.pipe.ngid]
        msg += ["node2=0x%s" % netif2.pipe.ngid, "}"]
        ngmessage(self.ngname, msg)
        self._linked[netif1][netif2] = True

    def linknet(self, net):
        """ Link this bridge with another by creating a veth pair and installing
            each device into each bridge.
        """
        raise NotImplementedError

    def linkconfig(self, netif, bw=None, delay=None,
                   loss=None, duplicate=None, jitter=None, netif2=None):
        """ Set link effects by modifying the pipe connected to an interface.
        """
        if not netif.pipe:
            self.warn("linkconfig for %s but interface %s has no pipe" % (self.name, netif.name))
            return
        return netif.pipe.linkconfig(netif, bw, delay, loss, duplicate, jitter, netif2)


class NetgraphPipeNet(NetgraphNet):
    ngtype = "pipe"
    nghooks = "upper lower"

    def __init__(self, session, objid=None, name=None, verbose=False,
                 start=True, policy=None):
        NetgraphNet.__init__(self, session, objid, name, verbose, start, policy)
        if start:
            # account for Ethernet header
            ngmessage(self.ngname, ["setcfg", "{", "header_offset=14", "}"])

    def attach(self, netif):
        """ Attach an interface to this pipe node.
            The first interface is connected to the "upper" hook, the second
            connected to the "lower" hook.
        """
        if len(self._netif) > 1:
            raise ValueError, \
                "Netgraph pipes support at most 2 network interfaces"
        if self.up:
            hook = self.gethook()
            connectngnodes(self.ngname, netif.localname, hook, netif.hook)
        if netif.pipe:
            raise ValueError, \
                "Interface %s already attached to pipe %s" % \
                (netif.name, netif.pipe.name)
        netif.pipe = self
        self._netif[netif] = netif
        self._linked[netif] = {}

    def attachnet(self, net, hook):
        """ Attach another NetgraphNet to this pipe node.
        """
        localhook = self.gethook()
        connectngnodes(self.ngname, net.ngname, localhook, hook)

    def gethook(self):
        """ Returns the first hook (e.g. "upper") then the second hook
            (e.g. "lower") based on the number of connections.
        """
        hooks = self.nghooks.split()
        if len(self._netif) == 0:
            return hooks[0]
        else:
            return hooks[1]

    def linkconfig(self, netif, bw=None, delay=None,
                   loss=None, duplicate=None, jitter=None, netif2=None):
        """ Set link effects by sending a Netgraph setcfg message to the pipe.
        """
        netif.setparam("bw", bw)
        netif.setparam("delay", delay)
        netif.setparam("loss", loss)
        netif.setparam("duplicate", duplicate)
        netif.setparam("jitter", jitter)
        if not self.up:
            return
        params = []
        upstream = []
        downstream = []
        if bw is not None:
            if str(bw) == "0":
                bw = "-1"
            params += ["bandwidth=%s" % bw, ]
        if delay is not None:
            if str(delay) == "0":
                delay = "-1"
            params += ["delay=%s" % delay, ]
        if loss is not None:
            if str(loss) == "0":
                loss = "-1"
            upstream += ["BER=%s" % loss, ]
            downstream += ["BER=%s" % loss, ]
        if duplicate is not None:
            if str(duplicate) == "0":
                duplicate = "-1"
            upstream += ["duplicate=%s" % duplicate, ]
            downstream += ["duplicate=%s" % duplicate, ]
        if jitter:
            self.warn("jitter parameter ignored for link %s" % self.name)
        if len(params) > 0 or len(upstream) > 0 or len(downstream) > 0:
            setcfg = ["setcfg", "{", ] + params
            if len(upstream) > 0:
                setcfg += ["upstream={", ] + upstream + ["}", ]
            if len(downstream) > 0:
                setcfg += ["downstream={", ] + downstream + ["}", ]
            setcfg += ["}", ]
            ngmessage(self.ngname, setcfg)