#
# Rumba - Topology Model
#
#    Copyright (C) 2017-2026 imec
#
#    Sander Vrijders   <sander.vrijders@ugent.be>
#    Dimitri Staessens <dimitri.staessens@ugent.be>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., http://www.fsf.org/about/contact/.
#

import threading
from enum import Enum

import rumba.log as log

logger = log.get_logger(__name__)


# ---------------------------------------------------------------------------
# Layer types (aligned with Ouroboros IPCP types)
# ---------------------------------------------------------------------------

class LayerType(Enum):
    """IPCP / layer types available in Ouroboros."""
    LOCAL     = 'local'
    UNICAST   = 'unicast'
    BROADCAST = 'broadcast'
    ETH_LLC   = 'eth-llc'
    ETH_DIX   = 'eth-dix'
    UDP4      = 'udp4'
    UDP6      = 'udp6'


# ---------------------------------------------------------------------------
# Layer classes
# ---------------------------------------------------------------------------

class Layer(object):
    """
    Base class for network layers.

    A Layer represents a network layer in an Ouroboros network.
    """

    def get_e_id(self):
        return "Layer." + self.name

    def __init__(self, name, layer_type=LayerType.UNICAST, members=None):
        """
        :param name: Name of the layer.
        :param layer_type: Type of the layer (LayerType enum).
        :param members: List of nodes that are members of this layer.
        """
        self.name = name
        self.layer_type = layer_type
        if members is None:
            members = list()
        self.members = members
        self.ipcps = list()

    def __repr__(self):
        s = "Layer %s (%s)" % (self.name, self.layer_type.value)
        return s

    def __hash__(self):
        return hash(self.name)

    def __eq__(self, other):
        return other is not None and self.name == other.name

    def __neq__(self, other):
        return not self == other

    def add_member(self, node):
        self.members.append(node)

    def del_member(self, node):
        self.members.remove(node)

    def get_ipcp_class(self):
        return IPCP

    @property
    def is_eth(self):
        return self.layer_type in (LayerType.ETH_DIX, LayerType.ETH_LLC)

    @property
    def is_udp(self):
        return self.layer_type in (LayerType.UDP4, LayerType.UDP6)

    @property
    def is_shim(self):
        return self.layer_type in (LayerType.ETH_DIX, LayerType.ETH_LLC,
                                   LayerType.UDP4, LayerType.UDP6,
                                   LayerType.LOCAL)


# Shim over UDP (IPv4)
class Udp4Layer(Layer):
    """
    Layer using the UDP over IPv4 shim IPCP.
    """
    def __init__(self, name, members=None):
        """
        :param name: Name of the layer.
        :param members: List of members of the layer.
        """
        Layer.__init__(self, name, LayerType.UDP4, members)

    def get_ipcp_class(self):
        return UdpIPCP


# Shim over UDP (IPv6)
class Udp6Layer(Layer):
    """
    Layer using the UDP over IPv6 shim IPCP.
    """
    def __init__(self, name, members=None):
        """
        :param name: Name of the layer.
        :param members: List of members of the layer.
        """
        Layer.__init__(self, name, LayerType.UDP6, members)

    def get_ipcp_class(self):
        return UdpIPCP


# Ethernet layer (point-to-point link)
class EthDixLayer(Layer):
    """
    Layer using an Ethernet shim IPCP (eth-dix).
    Models a point-to-point Ethernet link between two nodes.
    """
    def get_e_id(self):
        return "EthDixLayer." + self.name

    def __init__(self, name, members=None, link_quality=None,
                 layer_type=LayerType.ETH_DIX):
        """
        :param name: Name of the layer.
        :param members: List of members (max 2 for point-to-point).
        :param link_quality: Quality of the link.
        :param layer_type: LayerType (ETH_DIX or ETH_LLC).
        """
        Layer.__init__(self, name, layer_type, members)
        self._link_quality = link_quality if link_quality is not None \
            else LinkQuality()
        self._link_quality_lock = threading.RLock()

    def get_ipcp_class(self):
        return EthIPCP

    def add_member(self, node):
        super(EthDixLayer, self).add_member(node)
        if len(self.members) > 2:
            raise Exception("More than 2 members in %s!" % self.name)

    @property
    def link_quality(self):
        return self._link_quality

    @link_quality.setter
    def link_quality(self, _link_quality):
        if not _link_quality:
            raise ValueError("Cannot set link_quality to None, use del "
                             "link_quality to reset")
        with self._link_quality_lock:
            self._link_quality = _link_quality
            _link_quality.apply(self)

    @link_quality.deleter
    def link_quality(self):
        with self._link_quality_lock:
            self._link_quality.deactivate(self)
            self._link_quality = LinkQuality()

    def set_delay(self, delay=0,
                  jitter=None,
                  correlation=None,
                  distribution=None):
        """
        Set the delay parameters of the underlying link.
        Parameters as in :py:class:`.Delay`

        :param delay: average delay in ms
        :type delay: :py:class:`int`
        :param jitter: jitter in ms
        :type jitter: :py:class:`int`
        :param correlation: correlation in %
        :type correlation: :py:class:`int`
        :param distribution: delay distribution, defaults to Normal
        :type distribution: :py:class:`.Distribution`
        """
        new_delay = Delay(delay, jitter, correlation, distribution)
        with self._link_quality_lock:
            new_quality = LinkQuality.clone(
                self._link_quality, delay=new_delay)
            self.link_quality = new_quality

    def set_loss(self,
                 loss=0,
                 correlation=None):
        """
        Set the loss parameter of the underlying link.
        Parameters as in :py:class:`.Loss`

        :param loss: loss in percentage
        :type loss: :py:class:`int` or :py:class:`float`
        :param correlation: correlation in percentage
        :type correlation: :py:class:`int` or :py:class:`float`
        """
        new_loss = Loss(loss, correlation)
        with self._link_quality_lock:
            new_quality = LinkQuality.clone(
                self._link_quality, loss=new_loss)
            self.link_quality = new_quality

    def set_rate(self, rate=None):
        """
        Set the rate parameter of the underlying link.

        :param rate: The desired rate in mbps
        :type rate: :py:class:`int`
        """
        with self._link_quality_lock:
            new_quality = LinkQuality.clone(self._link_quality, rate=rate)
            self.link_quality = new_quality

    def set_duplicate(self, duplicate=0, correlation=None):
        """
        Set the duplication parameter of the underlying link.
        Parameters as in :py:class:`.Duplicate`

        :param duplicate: duplication in percentage
        :type duplicate: :py:class:`int` or :py:class:`float`
        :param correlation: correlation in percentage
        :type correlation: :py:class:`int` or :py:class:`float`
        """
        new_dup = Duplicate(duplicate, correlation)
        with self._link_quality_lock:
            new_quality = LinkQuality.clone(
                self._link_quality, duplicate=new_dup)
            self.link_quality = new_quality

    def set_reorder(self, reorder=0, correlation=None):
        """
        Set the reorder parameter of the underlying link.
        Parameters as in :py:class:`.Reorder`

        :param reorder: reorder probability in percentage
        :type reorder: :py:class:`int` or :py:class:`float`
        :param correlation: correlation in percentage
        :type correlation: :py:class:`int` or :py:class:`float`
        """
        new_reorder = Reorder(reorder, correlation)
        with self._link_quality_lock:
            new_quality = LinkQuality.clone(
                self._link_quality, reorder=new_reorder)
            self.link_quality = new_quality

    def set_corrupt(self, corrupt=0, correlation=None):
        """
        Set the corruption parameter of the underlying link.
        Parameters as in :py:class:`.Corrupt`

        :param corrupt: corruption probability in percentage
        :type corrupt: :py:class:`int` or :py:class:`float`
        :param correlation: correlation in percentage
        :type correlation: :py:class:`int` or :py:class:`float`
        """
        new_corrupt = Corrupt(corrupt, correlation)
        with self._link_quality_lock:
            new_quality = LinkQuality.clone(
                self._link_quality, corrupt=new_corrupt)
            self.link_quality = new_quality

    def set_quality(self, delay, loss, rate):
        """
        Configure the basic quality parameters of the underlying link.

        :param delay: the link delay, in ms
        :type delay: :py:class:`int`
        :param loss: the link loss, as a percentage
        :type loss: :py:class:`float` or :py:class:`int`
        :param rate: the link rate in mbps
        :type rate: :py:class:`int`
        """
        new_quality = LinkQuality(delay, loss, rate)
        with self._link_quality_lock:
            self.link_quality = new_quality


class EthLlcLayer(EthDixLayer):
    """
    Layer using an Ethernet shim IPCP (eth-llc).
    Models a point-to-point Ethernet link between two nodes.
    """
    def get_e_id(self):
        return "EthLlcLayer." + self.name

    def __init__(self, name, members=None, link_quality=None):
        """
        :param name: Name of the layer.
        :param members: List of members (max 2 for point-to-point).
        :param link_quality: Quality of the link.
        """
        super().__init__(name, members, link_quality, LayerType.ETH_LLC)


class UnicastLayer(Layer):
    """
    Unicast network layer.
    """
    def __init__(self,
                 name,
                 members=None,
                 policy=None,
                 add_default_qos_cubes=True):
        """
        :param name: The name of the layer.
        :param members: The list of members.
        :param policy: Policies of the layer.
        :param add_default_qos_cubes: should prototype-default QoS cubes
                                      be added?
        """
        Layer.__init__(self, name, LayerType.UNICAST, members)
        if policy is None:
            policy = Policy(self)
        self.policy = policy
        self.qos_cubes = []
        self.add_default_qos_cubes = add_default_qos_cubes
        self._last_cube_id = 0

    def add_policy(self, comp, pol, **params):
        """
        Adds a policy to the layer.

        :param comp: Component name.
        :param pol: Policy name
        :param params: Parameters of the policy.
        """
        self.policy.add_policy(comp, pol, **params)

    def del_policy(self, comp=None, pol=None):
        """
        Deletes a policy from the layer.

        :param comp: Component name.
        :param pol: Policy name
        """
        self.policy.del_policy(comp, pol)

    def show(self):
        """
        :return: A string representing the policies in the layer.
        """
        s = Layer.__repr__(self)
        for comp, pol_dict in self.policy.get_policies().items():
            for pol, params in pol_dict.items():
                s += "\n       Component %s has policy %s with params %s" \
                     % (comp, pol, params)
        return s

    def add_qos_cube(self, name, **kwargs):
        """
        Adds a QoS Cube to this layer.

        :param name: the name to be assigned to the QoS cube
        :type name: `str`
        :param kwargs: the parameters of the QoS cube (prototype dependent)
        """
        self.del_qos_cube(name, strict=False)
        c_id = self._last_cube_id + 1
        self._last_cube_id = c_id
        kwargs["name"] = name
        kwargs["cube_id"] = c_id
        self.qos_cubes.append(kwargs)

    def del_qos_cube(self, name, strict=True):
        """
        Deletes a QoS cube from this layer.

        :param name: the name of the cube to delete
        :type name: `str`
        :param strict: if True, raise if cube not found
        :type strict: `bool`
        """
        for i, cube in enumerate(self.qos_cubes):
            if cube["name"] == name:
                index = i
                break
        else:  # no match
            if strict:
                raise ValueError("No cube with name %s found in layer %s"
                                 % (name, self.name))
            else:
                return
        self.qos_cubes.pop(index)


class BroadcastLayer(Layer):
    """
    Broadcast network layer.
    """
    def __init__(self, name, members=None):
        """
        :param name: The name of the layer.
        :param members: The list of members.
        """
        Layer.__init__(self, name, LayerType.BROADCAST, members)


class LocalLayer(Layer):
    """
    Local layer (for local loopback testing).
    """
    def __init__(self, name, members=None):
        """
        :param name: The name of the layer.
        :param members: The list of members.
        """
        Layer.__init__(self, name, LayerType.LOCAL, members)


# ---------------------------------------------------------------------------
# Link quality classes
# ---------------------------------------------------------------------------

class Distribution(Enum):
    """
    An enum holding different statistical distributions.

    **Values:**

    `NORMAL = 1`

    `PARETO = 2`

    `PARETONORMAL = 3`
    """
    NORMAL = 1
    PARETO = 2
    PARETONORMAL = 3


class Delay(object):
    """
    A class representing delay of a link.
    """
    def __init__(self, delay=0, jitter=None, correlation=None,
                 distribution=None):
        """
        Configure link delay.

        :param delay: average delay in ms
        :type delay: :py:class:`int` or :py:class:`float`
        :param jitter: jitter in ms
        :type jitter: :py:class:`int`
        :param correlation: correlation in %
        :type correlation: :py:class:`int`
        :param distribution: delay distribution, defaults to Normal
        :type distribution: :py:class:`.Distribution`
        """
        if delay < 0:
            raise ValueError("Delay needs to be at least 0")
        if jitter and not jitter > 0:
            raise ValueError("Jitter needs to be higher than 0")
        if (not jitter) and correlation:
            raise ValueError("Correlation requires a value for jitter")
        if correlation and (correlation < 0 or correlation > 100):
            raise ValueError("Correlation needs to be between 0 and 100")
        self._delay = delay
        self._jitter = jitter
        self._correlation = correlation
        self._distribution = distribution

    @property
    def delay(self):
        return self._delay

    @property
    def jitter(self):
        return self._jitter

    @property
    def correlation(self):
        return self._correlation

    @property
    def distribution(self):
        return self._distribution

    def build_command(self):
        opts = ["delay %ims" % self.delay]
        if self.jitter:
            opts.append("%ims" % self.jitter)
            if self.correlation:
                opts.append("%f%%" % self.correlation)
        if self.distribution:
            opts.append("distribution %s" % self.distribution.name.lower())
        return " ".join(opts)


class Loss(object):
    """
    A class representing loss on a link.
    """
    def __init__(self, loss, correlation=None):
        """
        Configure link loss.

        :param loss: loss in percentage
        :type loss: :py:class:`int` or :py:class:`float`
        :param correlation: correlation in percentage
        :type correlation: :py:class:`int` or :py:class:`float`
        """
        if loss and (loss < 0 or loss > 100):
            raise ValueError("Loss needs to be between 0 and 100")
        if correlation and (correlation < 0 or correlation > 100):
            raise ValueError("Correlation needs to be between 0 and 100")
        self._loss = loss
        self._correlation = correlation

    @property
    def loss(self):
        return self._loss

    @property
    def correlation(self):
        return self._correlation

    def build_command(self):
        opts = ["loss %f%%" % self.loss]
        if self.correlation:
            opts.append("%f%%" % self.correlation)
        return " ".join(opts)


class Duplicate(object):
    """
    A class representing packet duplication on a link.
    """
    def __init__(self, duplicate, correlation=None):
        """
        Configure link duplication.

        :param duplicate: duplication in percentage
        :type duplicate: :py:class:`int` or :py:class:`float`
        :param correlation: correlation in percentage
        :type correlation: :py:class:`int` or :py:class:`float`
        """
        if duplicate and (duplicate < 0 or duplicate > 100):
            raise ValueError("Duplicate needs to be between 0 and 100")
        if correlation and (correlation < 0 or correlation > 100):
            raise ValueError("Correlation needs to be between 0 and 100")
        self._duplicate = duplicate
        self._correlation = correlation

    @property
    def duplicate(self):
        return self._duplicate

    @property
    def correlation(self):
        return self._correlation

    def build_command(self):
        opts = ["duplicate %f%%" % self.duplicate]
        if self.correlation:
            opts.append("%f%%" % self.correlation)
        return " ".join(opts)


class Reorder(object):
    """
    A class representing packet reordering on a link.
    """
    def __init__(self, reorder, correlation=None):
        """
        Configure link reordering.

        :param reorder: reorder probability in percentage
        :type reorder: :py:class:`int` or :py:class:`float`
        :param correlation: correlation in percentage
        :type correlation: :py:class:`int` or :py:class:`float`
        """
        if reorder and (reorder < 0 or reorder > 100):
            raise ValueError("Reorder needs to be between 0 and 100")
        if correlation and (correlation < 0 or correlation > 100):
            raise ValueError("Correlation needs to be between 0 and 100")
        self._reorder = reorder
        self._correlation = correlation

    @property
    def reorder(self):
        return self._reorder

    @property
    def correlation(self):
        return self._correlation

    def build_command(self):
        opts = ["reorder %f%%" % self.reorder]
        if self.correlation:
            opts.append("%f%%" % self.correlation)
        return " ".join(opts)


class Corrupt(object):
    """
    A class representing packet corruption on a link.
    """
    def __init__(self, corrupt, correlation=None):
        """
        Configure link corruption.

        :param corrupt: corruption probability in percentage
        :type corrupt: :py:class:`int` or :py:class:`float`
        :param correlation: correlation in percentage
        :type correlation: :py:class:`int` or :py:class:`float`
        """
        if corrupt and (corrupt < 0 or corrupt > 100):
            raise ValueError("Corrupt needs to be between 0 and 100")
        if correlation and (correlation < 0 or correlation > 100):
            raise ValueError("Correlation needs to be between 0 and 100")
        self._corrupt = corrupt
        self._correlation = correlation

    @property
    def corrupt(self):
        return self._corrupt

    @property
    def correlation(self):
        return self._correlation

    def build_command(self):
        opts = ["corrupt %f%%" % self.corrupt]
        if self.correlation:
            opts.append("%f%%" % self.correlation)
        return " ".join(opts)


class LinkQuality(object):
    """
    A class representing the link quality.
    """

    @classmethod
    def clone(cls, old_quality, delay=None, loss=None, rate=None,
              duplicate=None, reorder=None, corrupt=None):
        """
        Clone old_quality, updating with provided parameters.

        :param old_quality: A :py:class:`.LinkQuality` instance
        :param delay: Delay object or number (ms)
        :param loss: Loss object or number (%)
        :param rate: Rate in mbit
        :param duplicate: Duplicate object or number (%)
        :param reorder: Reorder object or number (%)
        :param corrupt: Corrupt object or number (%)
        :return: a new :py:class:`.LinkQuality` instance.
        """
        if delay is None:
            delay = old_quality.delay
        if loss is None:
            loss = old_quality.loss
        if rate is None:
            rate = old_quality.rate
        if duplicate is None:
            duplicate = old_quality.duplicate
        if reorder is None:
            reorder = old_quality.reorder
        if corrupt is None:
            corrupt = old_quality.corrupt
        return LinkQuality(delay, loss, rate, duplicate, reorder, corrupt)

    def __init__(self, delay=None, loss=None, rate=None,
                 duplicate=None, reorder=None, corrupt=None):
        """
        Link quality configuration.

        :param delay: Delay object or number (ms)
        :param loss: Loss object or number (%)
        :param rate: Rate in mbit
        :param duplicate: Duplicate object or number (%)
        :param reorder: Reorder object or number (%)
        :param corrupt: Corrupt object or number (%)
        """
        if rate and not rate > 0:
            raise ValueError("Rate needs to be higher than 0")
        if isinstance(delay, (int, float)):
            delay = Delay(delay)
        if isinstance(loss, (int, float)):
            loss = Loss(loss)
        if isinstance(duplicate, (int, float)):
            duplicate = Duplicate(duplicate)
        if isinstance(reorder, (int, float)):
            reorder = Reorder(reorder)
        if isinstance(corrupt, (int, float)):
            corrupt = Corrupt(corrupt)
        self._delay = delay
        self._loss = loss
        self._rate = rate
        self._duplicate = duplicate
        self._reorder = reorder
        self._corrupt = corrupt

    @property
    def delay(self):
        return self._delay

    @property
    def loss(self):
        return self._loss

    @property
    def rate(self):
        return self._rate

    @property
    def duplicate(self):
        return self._duplicate

    @property
    def reorder(self):
        return self._reorder

    @property
    def corrupt(self):
        return self._corrupt

    @staticmethod
    def _netem_target(ipcp):
        """Return (netem_dev, on_host).

        netem goes on the bridge-side interface (host_ifname) when there is
        one, else on the IPCP interface itself.  *on_host* is True when that
        interface lives on the host rather than in the IPCP's namespace, in
        which case the command must run through the node's host_executor.
        """
        host_ifname = getattr(ipcp, 'host_ifname', None)
        if host_ifname and host_ifname != ipcp.ifname:
            return host_ifname, True
        return ipcp.ifname, False

    @staticmethod
    def _run_host(ipcp, cmds, check=True):
        """Run host-side (bridge/netem) commands.

        Uses the node's host_executor when set (namespaced testbeds, where
        the bridge-side interface is on the host); otherwise falls back to
        the node executor, which for the non-namespaced testbeds already runs
        on the host -- so behaviour is unchanged there.
        """
        host_executor = getattr(ipcp.node, 'host_executor', None)
        for cmd in cmds:
            if host_executor is not None:
                host_executor.execute_command(ipcp.node, cmd, True,
                                              check=check)
            else:
                ipcp.node.execute_command(cmd, as_root=True, check=check)

    def build_commands(self, ipcp):
        """Split tc commands into (node_cmds, host_cmds).

        Rate limiting (HTB) goes on the IPCP interface so a blocking send
        back-pressures the source, and always runs in the IPCP's namespace.
        netem impairments go on the bridge-side interface (host_ifname) where
        frames are already orphaned -- netem's skb_orphan() there no longer
        defeats that back-pressure -- and run on the host for namespaced
        testbeds.  Testbeds without a bridge (e.g. UDP veths) have no
        host_ifname and chain netem under HTB on the IPCP interface.
        apply_to_ipcp() clears any prior qdiscs first, so these are plain adds.
        """
        node_cmds = []
        host_cmds = []
        netem_dev, on_host = self._netem_target(ipcp)

        if self.rate:
            node_cmds.append(
                "tc qdisc add dev %s root handle 1: htb default 1"
                % ipcp.ifname)
            node_cmds.append(
                "tc class add dev %s parent 1: classid 1:1 "
                "htb rate %imbit" % (ipcp.ifname, self.rate))

        netem_parts = [self.delay, self.loss,
                       self.duplicate, self.reorder, self.corrupt]
        if any(netem_parts):
            if self.rate and not on_host:
                qref = "parent 1:1"      # no bridge: chain under HTB
            else:
                qref = "root"
            netem_cmd = ["tc qdisc add dev %s %s netem limit 65536"
                         % (netem_dev, qref)]
            for part in netem_parts:
                if part:
                    netem_cmd.append(part.build_command())
            (host_cmds if on_host else node_cmds).append(" ".join(netem_cmd))

        return node_cmds, host_cmds

    def apply_to_ipcp(self, ipcp):
        """Apply this link quality configuration to a single *ipcp*."""
        if getattr(ipcp, 'node', None) is None:
            logger.error(
                "Could not apply LinkQuality to IPCP because "
                "ipcp.node is None")
            return
        if not ipcp.ifname:
            logger.error(
                "Could not apply LinkQuality to IPCP because "
                "the interface name is None")
            return
        if not (self.delay or self.loss or self.rate
                or self.duplicate or self.reorder or self.corrupt):
            self.deactivate_ipcp(ipcp)
        else:
            # Clear any prior qdiscs first (best-effort, both devices) so
            # the adds in build_commands never hit an existing root qdisc.
            self.deactivate_ipcp(ipcp)
            node_cmds, host_cmds = self.build_commands(ipcp)
            if node_cmds:
                ipcp.node.execute_commands(node_cmds, as_root=True)
            if host_cmds:
                self._run_host(ipcp, host_cmds)
            ipcp._netem_active = True

    def deactivate_ipcp(self, ipcp):
        """Remove any active netem/htb qdisc from a single *ipcp*."""
        if not getattr(ipcp, '_netem_active', False):
            return
        # Clear the flag up-front: teardown is best-effort, and we do not
        # want a later failure in the tc command to leave the IPCP stuck
        # in the "active" state.
        ipcp._netem_active = False
        if getattr(ipcp, 'node', None) is None:
            logger.error(
                "Could not remove LinkQuality from IPCP because "
                "ipcp.node is None")
            return
        if not ipcp.ifname:
            logger.error(
                "Could not remove LinkQuality from IPCP because "
                "the interface name is None")
            return
        # Best-effort teardown: HTB lives on the IPCP interface (node ns),
        # netem on the bridge-side interface (host for namespaced testbeds);
        # either may be absent, so do not check.
        ipcp.node.execute_command(
            "tc qdisc del dev %s root" % ipcp.ifname,
            as_root=True, check=False)
        netem_dev, on_host = self._netem_target(ipcp)
        if on_host:
            self._run_host(
                ipcp, ["tc qdisc del dev %s root" % netem_dev], check=False)

    def apply(self, layer):
        """Apply this link quality to all IPCPs in *layer*."""
        for ipcp in layer.ipcps:
            self.apply_to_ipcp(ipcp)

    def deactivate(self, layer):
        """Remove any active netem/htb qdisc from all IPCPs in *layer*."""
        for ipcp in layer.ipcps:
            self.deactivate_ipcp(ipcp)


# ---------------------------------------------------------------------------
# SSH configuration
# ---------------------------------------------------------------------------

class SSHConfig(object):
    def __init__(self, hostname, port=22, proxy_server=None):
        self.username = None
        self.password = None
        self.hostname = hostname
        self.port = port
        self.proxy_server = proxy_server
        self.client = None
        self.proxy_client = None

    def set_username(self, username):
        self.username = username

    def set_password(self, password):
        self.password = password


# ---------------------------------------------------------------------------
# Node
# ---------------------------------------------------------------------------

class Node(object):
    """
    A node (system) in the experiment.

    Nodes declare their layer memberships and registrations::

        a = Node("a",
                 layers=[n1, e1],
                 registrations={n1: [e1]})
    """

    def get_e_id(self):
        return "Node." + self.name

    def __init__(self, name, layers=None, registrations=None,
                 policies=None, machine_type=None):
        """
        :param name: Name of the node.
        :param layers: A list of layers the node is a member of.
        :param registrations: How layers are stacked (upper -> [lower]).
        :param policies: Policies specific to this node.
        :param machine_type: Type of machine (physical or virtual).
        """
        self.name = name
        if layers is None:
            layers = list()
        self.layers = layers
        for layer in self.layers:
            layer.add_member(self)
        if registrations is None:
            registrations = dict()
        self.registrations = registrations
        self.machine_type = machine_type
        self.ssh_config = SSHConfig(name)
        self.ipcps = []
        self.policies = dict()
        self.has_tcpdump = False
        if policies is None:
            policies = dict()
        for layer in self.layers:
            if hasattr(layer, 'policy'):
                self.policies[layer] = policies.get(layer, Policy(layer, self))

        self.executor = None  # set by testbed on swap_in
        # Optional executor for host-side link shaping.  When a testbed runs
        # nodes in their own namespace (nstb), the bridge-side (host_ifname)
        # interface lives on the host, not in the node's namespace, so netem
        # must be applied there.  None means "same namespace as executor".
        self.host_executor = None
        self.startup_command = None  # set by prototype

        self._validate()

    def get_ipcp_by_layer(self, layer):
        """
        :param layer: The layer to get the IPCP of.
        :return: The IPCP of the node that is in the layer.
        """
        for ipcp in self.ipcps:
            if ipcp.layer == layer:
                return ipcp

    def _undeclared_layer(self, layer):
        if layer not in self.layers:
            raise Exception("Invalid registration: node %s is not declared "
                            "to be part of layer %s"
                            % (self.name, layer.name))

    def _validate(self):
        for upper in self.registrations:
            self._undeclared_layer(upper)
            for lower in self.registrations[upper]:
                self._undeclared_layer(lower)

    def __repr__(self):
        s = "Node " + self.name + ":\n"

        s += "  Layers: [ "
        s += " ".join([l.name for l in self.layers])
        s += " ]\n"

        s += "  Registrations: [ "
        rl = []
        for upper in self.registrations:
            lowers = self.registrations[upper]
            x = "%s => [" % upper.name
            x += " ".join([lower.name for lower in lowers])
            x += "]"
            rl.append(x)
        s += ", ".join(rl)
        s += " ]\n"

        return s

    def __hash__(self):
        return hash(self.name)

    def __eq__(self, other):
        return other is not None and self.name == other.name

    def __neq__(self, other):
        return not self == other

    def add_layer(self, layer):
        """
        Adds a layer to this node.

        :param layer: The layer to add.
        """
        self.layers.append(layer)
        layer.add_member(self)
        if hasattr(layer, 'policy'):
            self.policies[layer] = Policy(layer, self)
        self._validate()

    def del_layer(self, layer):
        """
        Removes a layer from this node.

        :param layer: The layer to remove.
        """
        self.layers.remove(layer)
        layer.del_member(self)
        try:
            del self.policies[layer]
        except KeyError:
            pass
        self._validate()

    def add_registration(self, upper, lower):
        """
        Adds a layer registration.

        :param upper: The upper layer requesting IPC.
        :param lower: The lower layer providing IPC.
        """
        if upper not in self.registrations:
            self.registrations[upper] = []
        self.registrations[upper].append(lower)
        self._validate()

    def del_registration(self, upper, lower):
        """
        Removes a layer registration.

        :param upper: The upper layer.
        :param lower: The lower layer.
        """
        self.registrations[upper].remove(lower)
        self._validate()

    def add_policy(self, layer, component_name, policy_name, **parameters):
        """
        Adds a policy.

        :param layer: The layer.
        :param component_name: Name of the component.
        :param policy_name: Name of the policy.
        :param parameters: Parameters of the policy.
        """
        self.policies[layer].add_policy(
            component_name, policy_name, **parameters)

    def del_policy(self, layer, component_name=None, policy_name=None):
        """
        Removes a policy.

        :param layer: The layer.
        :param component_name: Name of the component.
        :param policy_name: Name of the policy.
        """
        self.policies[layer].del_policy(component_name, policy_name)

    def get_policy(self, layer):
        """
        :param layer: The layer to get the policy of.
        :return: Returns the policy.
        """
        return self.policies[layer]

    def execute_commands(self, commands, as_root=False, time_out=3,
                         use_proxy=False):
        """
        Execute a list of commands on the node.

        :param commands: A list of commands.
        :param as_root: Execute as root?
        :param time_out: Seconds before timing out.
        :param use_proxy: Use a proxy?
        """
        return self.executor.execute_commands(
            self, commands, as_root, time_out)

    def execute_command(self, command, as_root=False, time_out=3,
                        use_proxy=False, check=True):
        """
        Execute a single command on the node.

        :param command: A command.
        :param as_root: Execute as root?
        :param time_out: Seconds before timing out.
        :param use_proxy: Use a proxy?
        :param check: If True, raise CommandError on non-zero exit.
        :return: A CommandResult.
        """
        return self.executor.execute_command(
            self, command, as_root, time_out, check=check)

    def copy_file(self, path, destination):
        """Copy file to node."""
        self.executor.copy_file(self, path, destination)

    def copy_files(self, paths, destination):
        """Copy files to node."""
        self.executor.copy_files(self, paths, destination)

    def fetch_file(self, path, destination, sudo=False):
        """Fetch file from node."""
        self.executor.fetch_file(self, path, destination, sudo)

    def fetch_files(self, paths, destination, sudo=False):
        """Fetch files from node."""
        self.executor.fetch_files(self, paths, destination, sudo)

    def set_link_state(self, layer, state):
        """
        Change the state of a link on the node.

        :param layer: The Ethernet layer.
        :param state: Up or down.
        """
        ipcp = self.get_ipcp_by_layer(layer)
        self.execute_command(
            'ip link set dev ' + ipcp.ifname + ' ' + state,
            as_root=True)


# ---------------------------------------------------------------------------
# IPCP classes
# ---------------------------------------------------------------------------

class IPCP(object):
    def __init__(self, name, node, layer):
        self.name = name
        self.node = node
        self.layer = layer
        self.registrations = []
        self.bootstrapper = False

    def __repr__(self):
        return "{IPCP=%s,Layer=%s,N-1-Layers=(%s)%s}" % \
               (self.name, self.layer.name,
                ' '.join([l.name for l in self.registrations]),
                ',bootstrapper' if self.bootstrapper else '')

    def __hash__(self):
        return hash((self.name, self.layer.name))

    def __eq__(self, other):
        return other is not None and self.name == other.name \
               and self.layer == other.layer

    def __neq__(self, other):
        return not self == other


class EthIPCP(IPCP):
    def __init__(self, name, node, layer, ifname=None):
        IPCP.__init__(self, name, node, layer)
        self.ifname = ifname


class UdpIPCP(IPCP):
    def __init__(self, name, node, layer, ip_addr=None, ifname=None):
        IPCP.__init__(self, name, node, layer)
        self.ip_addr = ip_addr or "0.0.0.0"
        self.ifname = ifname


# ---------------------------------------------------------------------------
# Policy
# ---------------------------------------------------------------------------

class Policy(object):
    def __init__(self, layer, node=None, policies=None):
        self.layer = layer
        self.node = node
        if policies is None:
            self._dict = dict()
        else:
            self._dict = policies

    def add_policy(self, component_name, policy_name, **parameters):
        self._dict.setdefault(
            component_name, dict())[policy_name] = parameters

    def get_policies(self, component_name=None, policy_name=None):
        policy = self._superimpose()
        if component_name is None:
            return policy._dict
        elif policy_name is None:
            return policy._dict[component_name]
        else:
            return policy._dict[component_name][policy_name]

    def del_policy(self, component_name=None, policy_name=None):
        if component_name is None:
            self._dict = dict()
        elif policy_name is None:
            del self._dict[component_name]
        else:
            del self._dict[component_name][policy_name]

    def _superimpose(self):
        if self.node is None:
            return self
        other = self.layer.policy
        base = dict(other._dict)
        base.update(self._dict)
        return Policy(self.layer, self.node, base)

    def __eq__(self, other):
        if not isinstance(other, Policy):
            return False
        return (other.layer == self.layer
                and other.node == self.node
                and other._dict == self._dict)

    def __str__(self):
        node_str = (" Node: " + self.node) if self.node is not None else ""
        return "Policy[Layer: %(layer)s,%(node_str)s Dict: %(dict)s]" \
               % {"layer": self.layer, "node_str": node_str,
                  "dict": self._dict}

    def __repr__(self):
        node_str = (" Node: " + self.node) if self.node is not None else ""
        s = "Policy[ Layer: %(layer)s,%(node_str)s" \
            % {"layer": self.layer, "node_str": node_str}
        comps = []
        for component in self._dict:
            for policy in self._dict[component]:
                comps.append(
                    "\n  Component %s has policy %s with params %s"
                    % (component, policy,
                       self._dict[component][policy]))
        s += ",".join(comps)
        s += "\n]\n"
        return s
