"""
The networking module for Debian-based distros

References:

* http://www.debian.org/doc/manuals/debian-reference/ch05.en.html
"""

# Import python libs

import functools
import logging
import os
import os.path
import re
import time

# Import third party libs
import jinja2
import jinja2.exceptions

# Import salt libs
import salt.utils.dns
import salt.utils.files
import salt.utils.odict
import salt.utils.stringutils
import salt.utils.templates
import salt.utils.validate.net
from salt.ext.six.moves import (  # pylint: disable=import-error,no-name-in-module
    StringIO,
)

# Set up logging
log = logging.getLogger(__name__)

# Set up template environment
JINJA = jinja2.Environment(
    loader=jinja2.FileSystemLoader(
        os.path.join(salt.utils.templates.TEMPLATE_DIRNAME, "debian_ip")
    )
)

# Define the module's virtual name
__virtualname__ = "ip"


def __virtual__():
    """
    Confine this module to Debian-based distros
    """
    if __grains__["os_family"] == "Debian":
        return __virtualname__
    return (False, "The debian_ip module could not be loaded: unsupported OS family")


_ETHTOOL_CONFIG_OPTS = {
    "speed": "link-speed",
    "duplex": "link-duplex",
    "autoneg": "ethernet-autoneg",
    "ethernet-port": "ethernet-port",
    "wol": "ethernet-wol",
    "driver-message-level": "driver-message-level",
    "ethernet-pause-rx": "ethernet-pause-rx",
    "ethernet-pause-tx": "ethernet-pause-tx",
    "ethernet-pause-autoneg": "ethernet-pause-autoneg",
    "rx": "offload-rx",
    "tx": "offload-tx",
    "sg": "offload-sg",
    "tso": "offload-tso",
    "ufo": "offload-ufo",
    "gso": "offload-gso",
    "gro": "offload-gro",
    "lro": "offload-lro",
    "hardware-irq-coalesce-adaptive-rx": "hardware-irq-coalesce-adaptive-rx",
    "hardware-irq-coalesce-adaptive-tx": "hardware-irq-coalesce-adaptive-tx",
    "hardware-irq-coalesce-rx-usecs": "hardware-irq-coalesce-rx-usecs",
    "hardware-irq-coalesce-rx-frames": "hardware-irq-coalesce-rx-frames",
    "hardware-dma-ring-rx": "hardware-dma-ring-rx",
    "hardware-dma-ring-rx-mini": "hardware-dma-ring-rx-mini",
    "hardware-dma-ring-rx-jumbo": "hardware-dma-ring-rx-jumbo",
    "hardware-dma-ring-tx": "hardware-dma-ring-tx",
}

_REV_ETHTOOL_CONFIG_OPTS = {
    "link-speed": "speed",
    "link-duplex": "duplex",
    "ethernet-autoneg": "autoneg",
    "ethernet-port": "ethernet-port",
    "ethernet-wol": "wol",
    "driver-message-level": "driver-message-level",
    "ethernet-pause-rx": "ethernet-pause-rx",
    "ethernet-pause-tx": "ethernet-pause-tx",
    "ethernet-pause-autoneg": "ethernet-pause-autoneg",
    "offload-rx": "rx",
    "offload-tx": "tx",
    "offload-sg": "sg",
    "offload-tso": "tso",
    "offload-ufo": "ufo",
    "offload-gso": "gso",
    "offload-lro": "lro",
    "offload-gro": "gro",
    "hardware-irq-coalesce-adaptive-rx": "hardware-irq-coalesce-adaptive-rx",
    "hardware-irq-coalesce-adaptive-tx": "hardware-irq-coalesce-adaptive-tx",
    "hardware-irq-coalesce-rx-usecs": "hardware-irq-coalesce-rx-usecs",
    "hardware-irq-coalesce-rx-frames": "hardware-irq-coalesce-rx-frames",
    "hardware-dma-ring-rx": "hardware-dma-ring-rx",
    "hardware-dma-ring-rx-mini": "hardware-dma-ring-rx-mini",
    "hardware-dma-ring-rx-jumbo": "hardware-dma-ring-rx-jumbo",
    "hardware-dma-ring-tx": "hardware-dma-ring-tx",
}

_DEB_CONFIG_PPPOE_OPTS = {
    "user": "user",
    "password": "password",
    "provider": "provider",
    "pppoe_iface": "pppoe_iface",
    "noipdefault": "noipdefault",
    "usepeerdns": "usepeerdns",
    "defaultroute": "defaultroute",
    "holdoff": "holdoff",
    "maxfail": "maxfail",
    "hide-password": "hide-password",
    "lcp-echo-interval": "lcp-echo-interval",
    "lcp-echo-failure": "lcp-echo-failure",
    "connect": "connect",
    "noauth": "noauth",
    "persist": "persist",
    "mtu": "mtu",
    "noaccomp": "noaccomp",
    "linkname": "linkname",
}

_DEB_ROUTES_FILE = "/etc/network/routes"
_DEB_NETWORK_FILE = "/etc/network/interfaces"
_DEB_NETWORK_DIR = "/etc/network/interfaces.d/"
_DEB_NETWORK_UP_DIR = "/etc/network/if-up.d/"
_DEB_NETWORK_DOWN_DIR = "/etc/network/if-down.d/"
_DEB_NETWORK_CONF_FILES = "/etc/modprobe.d/"
_DEB_NETWORKING_FILE = "/etc/default/networking"
_DEB_HOSTNAME_FILE = "/etc/hostname"
_DEB_RESOLV_FILE = "/etc/resolv.conf"
_DEB_PPP_DIR = "/etc/ppp/peers/"

_CONFIG_TRUE = ["yes", "on", "true", "1", True]
_CONFIG_FALSE = ["no", "off", "false", "0", False]
_IFACE_TYPES = [
    "eth",
    "bond",
    "alias",
    "clone",
    "ipsec",
    "dialup",
    "bridge",
    "slave",
    "vlan",
    "pppoe",
    "source",
]


def _error_msg_iface(iface, option, expected):
    """
    Build an appropriate error message from a given option and
    a list of expected values.
    """
    msg = "Invalid option -- Interface: {0}, Option: {1}, Expected: [{2}]"
    return msg.format(iface, option, "|".join(str(e) for e in expected))


def _error_msg_routes(iface, option, expected):
    """
    Build an appropriate error message from a given option and
    a list of expected values.
    """
    msg = "Invalid option -- Route interface: {0}, Option: {1}, Expected: [{2}]"
    return msg.format(iface, option, expected)


def _log_default_iface(iface, opt, value):
    log.info(
        "Using default option -- Interface: %s Option: %s Value: %s", iface, opt, value
    )


def _error_msg_network(option, expected):
    """
    Build an appropriate error message from a given option and
    a list of expected values.
    """
    msg = "Invalid network setting -- Setting: {0}, Expected: [{1}]"
    return msg.format(option, "|".join(str(e) for e in expected))


def _log_default_network(opt, value):
    log.info("Using existing setting -- Setting: %s Value: %s", opt, value)


def _raise_error_iface(iface, option, expected):
    """
    Log and raise an error with a logical formatted message.
    """
    msg = _error_msg_iface(iface, option, expected)
    log.error(msg)
    raise AttributeError(msg)


def _raise_error_network(option, expected):
    """
    Log and raise an error with a logical formatted message.
    """
    msg = _error_msg_network(option, expected)
    log.error(msg)
    raise AttributeError(msg)


def _raise_error_routes(iface, option, expected):
    """
    Log and raise an error with a logical formatted message.
    """
    msg = _error_msg_routes(iface, option, expected)
    log.error(msg)
    raise AttributeError(msg)


def _read_file(path):
    """
    Reads and returns the contents of a text file
    """
    try:
        with salt.utils.files.flopen(path, "rb") as contents:
            return [
                salt.utils.stringutils.to_str(line) for line in contents.readlines()
            ]
    except OSError:
        return ""


def _parse_resolve():
    """
    Parse /etc/resolv.conf
    """
    return salt.utils.dns.parse_resolv(_DEB_RESOLV_FILE)


def _parse_domainname():
    """
    Parse /etc/resolv.conf and return domainname
    """
    return _parse_resolve().get("domain", "")


def _parse_searchdomain():
    """
    Parse /etc/resolv.conf and return searchdomain
    """
    return _parse_resolve().get("search", "")


def _parse_hostname():
    """
    Parse /etc/hostname and return hostname
    """
    contents = _read_file(_DEB_HOSTNAME_FILE)
    if contents:
        return contents[0].split("\n")[0]
    else:
        return ""


def _parse_current_network_settings():
    """
    Parse /etc/default/networking and return current configuration
    """
    opts = salt.utils.odict.OrderedDict()
    opts["networking"] = ""

    if os.path.isfile(_DEB_NETWORKING_FILE):
        with salt.utils.files.fopen(_DEB_NETWORKING_FILE) as contents:
            for line in contents:
                salt.utils.stringutils.to_unicode(line)
                if line.startswith("#"):
                    continue
                elif line.startswith("CONFIGURE_INTERFACES"):
                    opts["networking"] = line.split("=", 1)[1].strip()

    hostname = _parse_hostname()
    domainname = _parse_domainname()
    searchdomain = _parse_searchdomain()

    opts["hostname"] = hostname
    opts["domainname"] = domainname
    opts["searchdomain"] = searchdomain
    return opts


# def __validator_func(value):
#    return (valid: True/False, (transformed) value, error message)


def __ipv4_quad(value):
    """validate an IPv4 address"""
    return (salt.utils.validate.net.ipv4_addr(value), value, "dotted IPv4 address")


def __ipv6(value):
    """validate an IPv6 address"""
    return (salt.utils.validate.net.ipv6_addr(value), value, "IPv6 address")


def __mac(value):
    """validate a mac address"""
    return (salt.utils.validate.net.mac(value), value, "MAC address")


def __anything(value):
    return (True, value, None)


def __int(value):
    """validate an integer"""
    valid, _value = False, value
    try:
        _value = int(value)
        valid = True
    except ValueError:
        pass
    return (valid, _value, "integer")


def __float(value):
    """validate a float"""
    valid, _value = False, value
    try:
        _value = float(value)
        valid = True
    except ValueError:
        pass
    return (valid, _value, "float")


def __ipv4_netmask(value):
    """validate an IPv4 dotted quad or integer CIDR netmask"""
    valid, errmsg = False, "dotted quad or integer CIDR (0->32)"
    valid, value, _ = __int(value)
    if not (valid and 0 <= value <= 32):
        valid = salt.utils.validate.net.netmask(value)
    return (valid, value, errmsg)


def __ipv6_netmask(value):
    """validate an IPv6 integer netmask"""
    valid, errmsg = False, "IPv6 netmask (0->128)"
    valid, value, _ = __int(value)
    valid = valid and 0 <= value <= 128
    return (valid, value, errmsg)


def __within2(value, within=None, errmsg=None, dtype=None):
    """validate that a value is in ``within`` and optionally a ``dtype``"""
    valid, _value = False, value
    if dtype:
        try:
            _value = dtype(value)  # TODO: this is a bit loose when dtype is a class
            valid = _value in within
        except ValueError:
            pass
    else:
        valid = _value in within
    if errmsg is None:
        if dtype:
            typename = getattr(
                dtype,
                "__name__",
                hasattr(dtype, "__class__") and getattr(dtype.__class__, "name", dtype),
            )
            errmsg = "{} within '{}'".format(typename, within)
        else:
            errmsg = "within '{}'".format(within)
    return (valid, _value, errmsg)


def __within(within=None, errmsg=None, dtype=None):
    return functools.partial(__within2, within=within, errmsg=errmsg, dtype=dtype)


def __space_delimited_list(value):
    """validate that a value contains one or more space-delimited values"""
    if isinstance(value, str):
        value = value.strip().split()

    if hasattr(value, "__iter__") and value != []:
        return (True, value, "space-delimited string")
    else:
        return (
            False,
            value,
            "{} is not a valid space-delimited value.\n".format(value),
        )


SALT_ATTR_TO_DEBIAN_ATTR_MAP = {
    "dns": "dns-nameservers",
    "search": "dns-search",
    "hwaddr": "hwaddress",  # TODO: this limits bootp functionality
    "ipaddr": "address",
    "ipaddrs": "addresses",
}


DEBIAN_ATTR_TO_SALT_ATTR_MAP = {v: k for (k, v) in SALT_ATTR_TO_DEBIAN_ATTR_MAP.items()}

# TODO
DEBIAN_ATTR_TO_SALT_ATTR_MAP["address"] = "address"
DEBIAN_ATTR_TO_SALT_ATTR_MAP["hwaddress"] = "hwaddress"

IPV4_VALID_PROTO = ["bootp", "dhcp", "static", "manual", "loopback", "ppp"]

IPV4_ATTR_MAP = {
    "proto": __within(IPV4_VALID_PROTO, dtype=str),
    # ipv4 static & manual
    "address": __ipv4_quad,
    "addresses": __anything,
    "netmask": __ipv4_netmask,
    "broadcast": __ipv4_quad,
    "metric": __int,
    "gateway": __ipv4_quad,  # supports a colon-delimited list
    "pointopoint": __ipv4_quad,
    "hwaddress": __mac,
    "mtu": __int,
    "scope": __within(["global", "link", "host"], dtype=str),
    # dhcp
    "hostname": __anything,
    "leasehours": __int,
    "leasetime": __int,
    "vendor": __anything,
    "client": __anything,
    # bootp
    "bootfile": __anything,
    "server": __ipv4_quad,
    "hwaddr": __mac,
    # tunnel
    "mode": __within(["gre", "GRE", "ipip", "IPIP", "802.3ad"], dtype=str),
    "endpoint": __ipv4_quad,
    "dstaddr": __ipv4_quad,
    "local": __ipv4_quad,
    "ttl": __int,
    # bond
    "slaves": __anything,
    # ppp
    "provider": __anything,
    "unit": __int,
    "options": __anything,
    # resolvconf
    "dns-nameservers": __space_delimited_list,
    "dns-search": __space_delimited_list,
    #
    "vlan-raw-device": __anything,
    #
    "network": __anything,  # i don't know what this is
    "test": __anything,  # TODO
    "enable_ipv4": __anything,  # TODO
    "enable_ipv6": __anything,  # TODO
}


IPV6_VALID_PROTO = ["auto", "loopback", "static", "manual", "dhcp", "v4tunnel", "6to4"]

IPV6_ATTR_MAP = {
    "proto": __within(IPV6_VALID_PROTO),
    # ipv6 static & manual
    "address": __ipv6,
    "addresses": __anything,
    "netmask": __ipv6_netmask,
    "broadcast": __ipv6,
    "gateway": __ipv6,  # supports a colon-delimited list
    "hwaddress": __mac,
    "mtu": __int,
    "scope": __within(["global", "site", "link", "host"], dtype=str),
    # inet6 auto
    "privext": __within([0, 1, 2], dtype=int),
    "dhcp": __within([0, 1], dtype=int),
    # inet6 static & manual & dhcp
    "media": __anything,
    "accept_ra": __within([0, 1, 2], dtype=int),
    "autoconf": __within([0, 1], dtype=int),
    "preferred-lifetime": __int,
    "dad-attempts": __int,  # 0 to disable
    "dad-interval": __float,
    # bond
    "slaves": __anything,
    # tunnel
    "mode": __within(["gre", "GRE", "ipip", "IPIP", "802.3ad"], dtype=str),
    "endpoint": __ipv4_quad,
    "local": __ipv4_quad,
    "ttl": __int,
    # resolvconf
    "dns-nameservers": __space_delimited_list,
    "dns-search": __space_delimited_list,
    #
    "vlan-raw-device": __anything,
    "test": __anything,  # TODO
    "enable_ipv4": __anything,  # TODO
    "enable_ipv6": __anything,  # TODO
}


WIRELESS_ATTR_MAP = {
    "wireless-essid": __anything,
    "wireless-mode": __anything,  # TODO
    "wpa-ap-scan": __within([0, 1, 2], dtype=int),  # TODO
    "wpa-conf": __anything,
    "wpa-driver": __anything,
    "wpa-group": __anything,
    "wpa-key-mgmt": __anything,
    "wpa-pairwise": __anything,
    "wpa-psk": __anything,
    "wpa-proto": __anything,  # partial(__within,
    "wpa-roam": __anything,
    "wpa-ssid": __anything,  # TODO
}

ATTRMAPS = {
    "inet": [IPV4_ATTR_MAP, WIRELESS_ATTR_MAP],
    "inet6": [IPV6_ATTR_MAP, WIRELESS_ATTR_MAP],
}


def _validate_interface_option(attr, value, addrfam="inet"):
    """lookup the validation function for a [addrfam][attr] and
    return the results

    :param attr: attribute name
    :param value: raw setting value
    :param addrfam: address family (inet, inet6,
    """
    valid, _value, errmsg = False, value, "Unknown validator"
    attrmaps = ATTRMAPS.get(addrfam, [])
    for attrmap in attrmaps:
        if attr in attrmap:
            validate_func = attrmap[attr]
            (valid, _value, errmsg) = validate_func(value)
            break
    return (valid, _value, errmsg)


def _attrmaps_contain_attr(attr):
    return attr in WIRELESS_ATTR_MAP or attr in IPV4_ATTR_MAP or attr in IPV6_ATTR_MAP


def _parse_interfaces(interface_files=None):
    """
    Parse /etc/network/interfaces and return current configured interfaces
    """
    if interface_files is None:
        interface_files = []
        # Add this later.
        if os.path.exists(_DEB_NETWORK_DIR):
            interface_files += [
                "{}/{}".format(_DEB_NETWORK_DIR, dir)
                for dir in os.listdir(_DEB_NETWORK_DIR)
            ]

        if os.path.isfile(_DEB_NETWORK_FILE):
            interface_files.insert(0, _DEB_NETWORK_FILE)

    adapters = salt.utils.odict.OrderedDict()
    method = -1

    for interface_file in interface_files:
        with salt.utils.files.fopen(interface_file) as interfaces:
            # This ensures iface_dict exists, but does not ensure we're not reading a new interface.
            iface_dict = {}
            for line in interfaces:
                line = salt.utils.stringutils.to_unicode(line)
                # Identify the clauses by the first word of each line.
                # Go to the next line if the current line is a comment
                # or all spaces.
                if line.lstrip().startswith("#") or line.isspace():
                    continue
                # Parse the iface clause
                if line.startswith("iface"):
                    sline = line.split()

                    if len(sline) != 4:
                        msg = "Interface file malformed: {0}."
                        msg = msg.format(sline)
                        log.error(msg)
                        raise AttributeError(msg)

                    iface_name = sline[1]
                    addrfam = sline[2]
                    method = sline[3]

                    # Create item in dict, if not already there
                    if iface_name not in adapters:
                        adapters[iface_name] = salt.utils.odict.OrderedDict()

                    # Create item in dict, if not already there
                    if "data" not in adapters[iface_name]:
                        adapters[iface_name]["data"] = salt.utils.odict.OrderedDict()

                    if addrfam not in adapters[iface_name]["data"]:
                        adapters[iface_name]["data"][
                            addrfam
                        ] = salt.utils.odict.OrderedDict()

                    iface_dict = adapters[iface_name]["data"][addrfam]

                    iface_dict["addrfam"] = addrfam
                    iface_dict["proto"] = method
                    iface_dict["filename"] = interface_file

                # Parse the detail clauses.
                elif line[0].isspace():
                    sline = line.split()

                    # conf file attr: dns-nameservers
                    # salt states.network attr: dns

                    attr, valuestr = line.rstrip().split(None, 1)
                    if _attrmaps_contain_attr(attr):
                        if "-" in attr:
                            attrname = attr.replace("-", "_")
                        else:
                            attrname = attr
                        (valid, value, errmsg) = _validate_interface_option(
                            attr, valuestr, addrfam
                        )
                        if attrname == "address" and "address" in iface_dict:
                            if "addresses" not in iface_dict:
                                iface_dict["addresses"] = []
                            iface_dict["addresses"].append(value)
                        else:
                            iface_dict[attrname] = value

                    elif attr in _REV_ETHTOOL_CONFIG_OPTS:
                        if "ethtool" not in iface_dict:
                            iface_dict["ethtool"] = salt.utils.odict.OrderedDict()
                        iface_dict["ethtool"][attr] = valuestr

                    elif attr.startswith("bond"):
                        opt = re.split(r"[_-]", attr, maxsplit=1)[1]
                        if "bonding" not in iface_dict:
                            iface_dict["bonding"] = salt.utils.odict.OrderedDict()
                        iface_dict["bonding"][opt] = valuestr

                    elif attr.startswith("bridge"):
                        opt = re.split(r"[_-]", attr, maxsplit=1)[1]
                        if "bridging" not in iface_dict:
                            iface_dict["bridging"] = salt.utils.odict.OrderedDict()
                        iface_dict["bridging"][opt] = valuestr

                    elif attr in [
                        "up",
                        "pre-up",
                        "post-up",
                        "down",
                        "pre-down",
                        "post-down",
                    ]:
                        cmd = valuestr
                        cmd_key = "{}_cmds".format(re.sub("-", "_", attr))
                        if cmd_key not in iface_dict:
                            iface_dict[cmd_key] = []
                        iface_dict[cmd_key].append(cmd)

                elif line.startswith("auto"):
                    for word in line.split()[1:]:
                        if word not in adapters:
                            adapters[word] = salt.utils.odict.OrderedDict()
                        adapters[word]["enabled"] = True

                elif line.startswith("allow-hotplug"):
                    for word in line.split()[1:]:
                        if word not in adapters:
                            adapters[word] = salt.utils.odict.OrderedDict()
                        adapters[word]["hotplug"] = True

                elif line.startswith("source"):
                    if "source" not in adapters:
                        adapters["source"] = salt.utils.odict.OrderedDict()

                    # Create item in dict, if not already there
                    if "data" not in adapters["source"]:
                        adapters["source"]["data"] = salt.utils.odict.OrderedDict()
                        adapters["source"]["data"]["sources"] = []
                    adapters["source"]["data"]["sources"].append(line.split()[1])

    # Return a sorted list of the keys for bond, bridge and ethtool options to
    # ensure a consistent order
    for iface_name in adapters:
        if iface_name == "source":
            continue
        if "data" not in adapters[iface_name]:
            msg = "Interface file malformed for interface: {}.".format(iface_name)
            log.error(msg)
            adapters.pop(iface_name)
            continue
        for opt in ["ethtool", "bonding", "bridging"]:
            for inet in ["inet", "inet6"]:
                if inet in adapters[iface_name]["data"]:
                    if opt in adapters[iface_name]["data"][inet]:
                        opt_keys = sorted(
                            adapters[iface_name]["data"][inet][opt].keys()
                        )
                        adapters[iface_name]["data"][inet][opt + "_keys"] = opt_keys

    return adapters


def _parse_ethtool_opts(opts, iface):
    """
    Filters given options and outputs valid settings for ETHTOOLS_OPTS
    If an option has a value that is not expected, this
    function will log what the Interface, Setting and what it was
    expecting.
    """
    config = {}

    if "autoneg" in opts:
        if opts["autoneg"] in _CONFIG_TRUE:
            config.update({"autoneg": "on"})
        elif opts["autoneg"] in _CONFIG_FALSE:
            config.update({"autoneg": "off"})
        else:
            _raise_error_iface(iface, "autoneg", _CONFIG_TRUE + _CONFIG_FALSE)

    if "duplex" in opts:
        valid = ["full", "half"]
        if opts["duplex"] in valid:
            config.update({"duplex": opts["duplex"]})
        else:
            _raise_error_iface(iface, "duplex", valid)

    if "speed" in opts:
        valid = ["10", "100", "1000", "10000"]
        if str(opts["speed"]) in valid:
            config.update({"speed": opts["speed"]})
        else:
            _raise_error_iface(iface, opts["speed"], valid)

    valid = _CONFIG_TRUE + _CONFIG_FALSE
    for option in ("rx", "tx", "sg", "tso", "ufo", "gso", "gro", "lro"):
        if option in opts:
            if opts[option] in _CONFIG_TRUE:
                config.update({option: "on"})
            elif opts[option] in _CONFIG_FALSE:
                config.update({option: "off"})
            else:
                _raise_error_iface(iface, option, valid)

    return config


def _parse_ethtool_pppoe_opts(opts, iface):
    """
    Filters given options and outputs valid settings for ETHTOOLS_PPPOE_OPTS
    If an option has a value that is not expected, this
    function will log what the Interface, Setting and what it was
    expecting.
    """
    config = {}

    for opt in _DEB_CONFIG_PPPOE_OPTS:
        if opt in opts:
            config[opt] = opts[opt]

    if "provider" in opts and not opts["provider"]:
        _raise_error_iface(iface, "provider", _CONFIG_TRUE + _CONFIG_FALSE)

    valid = _CONFIG_TRUE + _CONFIG_FALSE
    for option in (
        "noipdefault",
        "usepeerdns",
        "defaultroute",
        "hide-password",
        "noauth",
        "persist",
        "noaccomp",
    ):
        if option in opts:
            if opts[option] in _CONFIG_TRUE:
                config.update({option: "True"})
            elif opts[option] in _CONFIG_FALSE:
                config.update({option: "False"})
            else:
                _raise_error_iface(iface, option, valid)

    return config


def _parse_settings_bond(opts, iface):
    """
    Filters given options and outputs valid settings for requested
    operation. If an option has a value that is not expected, this
    function will log what the Interface, Setting and what it was
    expecting.
    """

    bond_def = {
        # 803.ad aggregation selection logic
        # 0 for stable (default)
        # 1 for bandwidth
        # 2 for count
        "ad_select": "0",
        # Max number of transmit queues (default = 16)
        "tx_queues": "16",
        # Link monitoring in milliseconds. Most NICs support this
        "miimon": "100",
        # ARP interval in milliseconds
        "arp_interval": "250",
        # Delay before considering link down in milliseconds (miimon * 2)
        "downdelay": "200",
        # lacp_rate 0: Slow - every 30 seconds
        # lacp_rate 1: Fast - every 1 second
        "lacp_rate": "0",
        # Max bonds for this driver
        "max_bonds": "1",
        # Specifies the time, in milliseconds, to wait before
        # enabling a slave after a link recovery has been
        # detected. Only used with miimon.
        "updelay": "0",
        # Used with miimon.
        # On: driver sends mii
        # Off: ethtool sends mii
        "use_carrier": "on",
        # Default. Don't change unless you know what you are doing.
        "xmit_hash_policy": "layer2",
    }

    if opts["mode"] in ["balance-rr", "0"]:
        log.info("Device: %s Bonding Mode: load balancing (round-robin)", iface)
        return _parse_settings_bond_0(opts, iface, bond_def)
    elif opts["mode"] in ["active-backup", "1"]:
        log.info("Device: %s Bonding Mode: fault-tolerance (active-backup)", iface)
        return _parse_settings_bond_1(opts, iface, bond_def)
    elif opts["mode"] in ["balance-xor", "2"]:
        log.info("Device: %s Bonding Mode: load balancing (xor)", iface)
        return _parse_settings_bond_2(opts, iface, bond_def)
    elif opts["mode"] in ["broadcast", "3"]:
        log.info("Device: %s Bonding Mode: fault-tolerance (broadcast)", iface)
        return _parse_settings_bond_3(opts, iface, bond_def)
    elif opts["mode"] in ["802.3ad", "4"]:
        log.info(
            "Device: %s Bonding Mode: IEEE 802.3ad Dynamic link " "aggregation", iface
        )
        return _parse_settings_bond_4(opts, iface, bond_def)
    elif opts["mode"] in ["balance-tlb", "5"]:
        log.info("Device: %s Bonding Mode: transmit load balancing", iface)
        return _parse_settings_bond_5(opts, iface, bond_def)
    elif opts["mode"] in ["balance-alb", "6"]:
        log.info("Device: %s Bonding Mode: adaptive load balancing", iface)
        return _parse_settings_bond_6(opts, iface, bond_def)
    else:
        valid = [
            "0",
            "1",
            "2",
            "3",
            "4",
            "5",
            "6",
            "balance-rr",
            "active-backup",
            "balance-xor",
            "broadcast",
            "802.3ad",
            "balance-tlb",
            "balance-alb",
        ]
        _raise_error_iface(iface, "mode", valid)


def _parse_settings_bond_0(opts, iface, bond_def):
    """
    Filters given options and outputs valid settings for bond0.
    If an option has a value that is not expected, this
    function will log what the Interface, Setting and what it was
    expecting.
    """
    bond = {"mode": "0"}

    # ARP targets in n.n.n.n form
    valid = ["list of ips (up to 16)"]
    if "arp_ip_target" in opts:
        if isinstance(opts["arp_ip_target"], list):
            if 1 <= len(opts["arp_ip_target"]) <= 16:
                bond.update({"arp_ip_target": ""})
                for ip in opts["arp_ip_target"]:  # pylint: disable=C0103
                    if len(bond["arp_ip_target"]) > 0:
                        bond["arp_ip_target"] = bond["arp_ip_target"] + "," + ip
                    else:
                        bond["arp_ip_target"] = ip
            else:
                _raise_error_iface(iface, "arp_ip_target", valid)
        else:
            _raise_error_iface(iface, "arp_ip_target", valid)
    else:
        _raise_error_iface(iface, "arp_ip_target", valid)

    if "arp_interval" in opts:
        try:
            int(opts["arp_interval"])
            bond.update({"arp_interval": opts["arp_interval"]})
        except ValueError:
            _raise_error_iface(iface, "arp_interval", ["integer"])
    else:
        _log_default_iface(iface, "arp_interval", bond_def["arp_interval"])
        bond.update({"arp_interval": bond_def["arp_interval"]})

    return bond


def _parse_settings_bond_1(opts, iface, bond_def):

    """
    Filters given options and outputs valid settings for bond1.
    If an option has a value that is not expected, this
    function will log what the Interface, Setting and what it was
    expecting.
    """
    bond = {"mode": "1"}

    for binding in ["miimon", "downdelay", "updelay"]:
        if binding in opts:
            try:
                int(opts[binding])
                bond.update({binding: opts[binding]})
            except ValueError:
                _raise_error_iface(iface, binding, ["integer"])
        else:
            _log_default_iface(iface, binding, bond_def[binding])
            bond.update({binding: bond_def[binding]})

    if "primary" in opts:
        bond.update({"primary": opts["primary"]})

    if not (__grains__["os"] == "Ubuntu" and __grains__["osrelease_info"][0] >= 16):
        if "use_carrier" in opts:
            if opts["use_carrier"] in _CONFIG_TRUE:
                bond.update({"use_carrier": "1"})
            elif opts["use_carrier"] in _CONFIG_FALSE:
                bond.update({"use_carrier": "0"})
            else:
                valid = _CONFIG_TRUE + _CONFIG_FALSE
                _raise_error_iface(iface, "use_carrier", valid)
        else:
            _log_default_iface(iface, "use_carrier", bond_def["use_carrier"])
            bond.update({"use_carrier": bond_def["use_carrier"]})

    return bond


def _parse_settings_bond_2(opts, iface, bond_def):
    """
    Filters given options and outputs valid settings for bond2.
    If an option has a value that is not expected, this
    function will log what the Interface, Setting and what it was
    expecting.
    """

    bond = {"mode": "2"}

    valid = ["list of ips (up to 16)"]
    if "arp_ip_target" in opts:
        if isinstance(opts["arp_ip_target"], list):
            if 1 <= len(opts["arp_ip_target"]) <= 16:
                bond.update({"arp_ip_target": ""})
                for ip in opts["arp_ip_target"]:  # pylint: disable=C0103
                    if len(bond["arp_ip_target"]) > 0:
                        bond["arp_ip_target"] = bond["arp_ip_target"] + "," + ip
                    else:
                        bond["arp_ip_target"] = ip
            else:
                _raise_error_iface(iface, "arp_ip_target", valid)
        else:
            _raise_error_iface(iface, "arp_ip_target", valid)
    else:
        _raise_error_iface(iface, "arp_ip_target", valid)

    if "arp_interval" in opts:
        try:
            int(opts["arp_interval"])
            bond.update({"arp_interval": opts["arp_interval"]})
        except ValueError:
            _raise_error_iface(iface, "arp_interval", ["integer"])
    else:
        _log_default_iface(iface, "arp_interval", bond_def["arp_interval"])
        bond.update({"arp_interval": bond_def["arp_interval"]})

    if "hashing-algorithm" in opts:
        valid = ["layer2", "layer2+3", "layer3+4"]
        if opts["hashing-algorithm"] in valid:
            bond.update({"xmit_hash_policy": opts["hashing-algorithm"]})
        else:
            _raise_error_iface(iface, "hashing-algorithm", valid)

    return bond


def _parse_settings_bond_3(opts, iface, bond_def):

    """
    Filters given options and outputs valid settings for bond3.
    If an option has a value that is not expected, this
    function will log what the Interface, Setting and what it was
    expecting.
    """
    bond = {"mode": "3"}

    for binding in ["miimon", "downdelay", "updelay"]:
        if binding in opts:
            try:
                int(opts[binding])
                bond.update({binding: opts[binding]})
            except ValueError:
                _raise_error_iface(iface, binding, ["integer"])
        else:
            _log_default_iface(iface, binding, bond_def[binding])
            bond.update({binding: bond_def[binding]})

    if "use_carrier" in opts:
        if opts["use_carrier"] in _CONFIG_TRUE:
            bond.update({"use_carrier": "1"})
        elif opts["use_carrier"] in _CONFIG_FALSE:
            bond.update({"use_carrier": "0"})
        else:
            valid = _CONFIG_TRUE + _CONFIG_FALSE
            _raise_error_iface(iface, "use_carrier", valid)
    else:
        _log_default_iface(iface, "use_carrier", bond_def["use_carrier"])
        bond.update({"use_carrier": bond_def["use_carrier"]})

    return bond


def _parse_settings_bond_4(opts, iface, bond_def):
    """
    Filters given options and outputs valid settings for bond4.
    If an option has a value that is not expected, this
    function will log what the Interface, Setting and what it was
    expecting.
    """

    bond = {"mode": "4"}

    for binding in ["miimon", "downdelay", "updelay", "lacp_rate", "ad_select"]:
        if binding in opts:
            if binding == "lacp_rate":
                if opts[binding] == "fast":
                    opts.update({binding: "1"})
                if opts[binding] == "slow":
                    opts.update({binding: "0"})
                valid = ["fast", "1", "slow", "0"]
            else:
                valid = ["integer"]
            try:
                int(opts[binding])
                bond.update({binding: opts[binding]})
            except ValueError:
                _raise_error_iface(iface, binding, valid)
        else:
            _log_default_iface(iface, binding, bond_def[binding])
            bond.update({binding: bond_def[binding]})

    if "use_carrier" in opts:
        if opts["use_carrier"] in _CONFIG_TRUE:
            bond.update({"use_carrier": "1"})
        elif opts["use_carrier"] in _CONFIG_FALSE:
            bond.update({"use_carrier": "0"})
        else:
            valid = _CONFIG_TRUE + _CONFIG_FALSE
            _raise_error_iface(iface, "use_carrier", valid)
    else:
        _log_default_iface(iface, "use_carrier", bond_def["use_carrier"])
        bond.update({"use_carrier": bond_def["use_carrier"]})

    if "hashing-algorithm" in opts:
        valid = ["layer2", "layer2+3", "layer3+4"]
        if opts["hashing-algorithm"] in valid:
            bond.update({"xmit_hash_policy": opts["hashing-algorithm"]})
        else:
            _raise_error_iface(iface, "hashing-algorithm", valid)

    return bond


def _parse_settings_bond_5(opts, iface, bond_def):

    """
    Filters given options and outputs valid settings for bond5.
    If an option has a value that is not expected, this
    function will log what the Interface, Setting and what it was
    expecting.
    """
    bond = {"mode": "5"}

    for binding in ["miimon", "downdelay", "updelay"]:
        if binding in opts:
            try:
                int(opts[binding])
                bond.update({binding: opts[binding]})
            except ValueError:
                _raise_error_iface(iface, binding, ["integer"])
        else:
            _log_default_iface(iface, binding, bond_def[binding])
            bond.update({binding: bond_def[binding]})

    if "use_carrier" in opts:
        if opts["use_carrier"] in _CONFIG_TRUE:
            bond.update({"use_carrier": "1"})
        elif opts["use_carrier"] in _CONFIG_FALSE:
            bond.update({"use_carrier": "0"})
        else:
            valid = _CONFIG_TRUE + _CONFIG_FALSE
            _raise_error_iface(iface, "use_carrier", valid)
    else:
        _log_default_iface(iface, "use_carrier", bond_def["use_carrier"])
        bond.update({"use_carrier": bond_def["use_carrier"]})

    if "primary" in opts:
        bond.update({"primary": opts["primary"]})

    return bond


def _parse_settings_bond_6(opts, iface, bond_def):

    """
    Filters given options and outputs valid settings for bond6.
    If an option has a value that is not expected, this
    function will log what the Interface, Setting and what it was
    expecting.
    """
    bond = {"mode": "6"}

    for binding in ["miimon", "downdelay", "updelay"]:
        if binding in opts:
            try:
                int(opts[binding])
                bond.update({binding: opts[binding]})
            except ValueError:
                _raise_error_iface(iface, binding, ["integer"])
        else:
            _log_default_iface(iface, binding, bond_def[binding])
            bond.update({binding: bond_def[binding]})

    if "use_carrier" in opts:
        if opts["use_carrier"] in _CONFIG_TRUE:
            bond.update({"use_carrier": "1"})
        elif opts["use_carrier"] in _CONFIG_FALSE:
            bond.update({"use_carrier": "0"})
        else:
            valid = _CONFIG_TRUE + _CONFIG_FALSE
            _raise_error_iface(iface, "use_carrier", valid)
    else:
        _log_default_iface(iface, "use_carrier", bond_def["use_carrier"])
        bond.update({"use_carrier": bond_def["use_carrier"]})

    if "primary" in opts:
        bond.update({"primary": opts["primary"]})

    return bond


def _parse_bridge_opts(opts, iface):
    """
    Filters given options and outputs valid settings for BRIDGING_OPTS
    If an option has a value that is not expected, this
    function will log the Interface, Setting and what was expected.
    """
    config = {}

    if "ports" in opts:
        if isinstance(opts["ports"], list):
            opts["ports"] = " ".join(opts["ports"])
        config.update({"ports": opts["ports"]})

    for opt in ["ageing", "fd", "gcint", "hello", "maxage"]:
        if opt in opts:
            try:
                float(opts[opt])
                config.update({opt: opts[opt]})
            except ValueError:
                _raise_error_iface(iface, opt, ["float"])

    for opt in ["bridgeprio", "maxwait"]:
        if opt in opts:
            if isinstance(opts[opt], int):
                config.update({opt: opts[opt]})
            else:
                _raise_error_iface(iface, opt, ["integer"])

    if "hw" in opts:
        # match 12 hex digits with either : or - as separators between pairs
        if re.match(
            "[0-9a-f]{2}([-:])[0-9a-f]{2}(\\1[0-9a-f]{2}){4}$", opts["hw"].lower()
        ):
            config.update({"hw": opts["hw"]})
        else:
            _raise_error_iface(iface, "hw", ["valid MAC address"])

    for opt in ["pathcost", "portprio"]:
        if opt in opts:
            try:
                port, cost_or_prio = opts[opt].split()
                int(cost_or_prio)
                config.update({opt: "{} {}".format(port, cost_or_prio)})
            except ValueError:
                _raise_error_iface(iface, opt, ["interface integer"])

    if "stp" in opts:
        if opts["stp"] in _CONFIG_TRUE:
            config.update({"stp": "on"})
        elif opts["stp"] in _CONFIG_FALSE:
            config.update({"stp": "off"})
        else:
            _raise_error_iface(iface, "stp", _CONFIG_TRUE + _CONFIG_FALSE)

    if "waitport" in opts:
        if isinstance(opts["waitport"], int):
            config.update({"waitport": opts["waitport"]})
        else:
            values = opts["waitport"].split()
            waitport_time = values.pop(0)
            if waitport_time.isdigit() and values:
                config.update(
                    {"waitport": "{} {}".format(waitport_time, " ".join(values))}
                )
            else:
                _raise_error_iface(iface, opt, ["integer [interfaces]"])

    return config


def _parse_settings_eth(opts, iface_type, enabled, iface):
    """
    Filters given options and outputs valid settings for a
    network interface.
    """
    adapters = salt.utils.odict.OrderedDict()
    adapters[iface] = salt.utils.odict.OrderedDict()

    adapters[iface]["type"] = iface_type

    adapters[iface]["data"] = salt.utils.odict.OrderedDict()
    iface_data = adapters[iface]["data"]
    iface_data["inet"] = salt.utils.odict.OrderedDict()
    iface_data["inet6"] = salt.utils.odict.OrderedDict()

    if enabled:
        adapters[iface]["enabled"] = True

    if opts.get("hotplug", False):
        adapters[iface]["hotplug"] = True

    if opts.get("enable_ipv6", None) and opts.get("iface_type", "") == "vlan":
        iface_data["inet6"]["vlan_raw_device"] = re.sub(r"\.\d*", "", iface)

    for addrfam in ["inet", "inet6"]:
        if iface_type not in ["bridge"]:
            tmp_ethtool = _parse_ethtool_opts(opts, iface)
            if tmp_ethtool:
                ethtool = {}
                for item in tmp_ethtool:
                    ethtool[_ETHTOOL_CONFIG_OPTS[item]] = tmp_ethtool[item]

                iface_data[addrfam]["ethtool"] = ethtool
                # return a list of sorted keys to ensure consistent order
                iface_data[addrfam]["ethtool_keys"] = sorted(ethtool)

        if iface_type == "bridge":
            bridging = _parse_bridge_opts(opts, iface)
            if bridging:
                iface_data[addrfam]["bridging"] = bridging
                iface_data[addrfam]["bridging_keys"] = sorted(bridging)
                iface_data[addrfam]["addrfam"] = addrfam

        elif iface_type == "bond":
            bonding = _parse_settings_bond(opts, iface)
            if bonding:
                iface_data[addrfam]["bonding"] = bonding
                iface_data[addrfam]["bonding"]["slaves"] = opts["slaves"]
                iface_data[addrfam]["bonding_keys"] = sorted(bonding)
                iface_data[addrfam]["addrfam"] = addrfam

        elif iface_type == "slave":
            adapters[iface]["master"] = opts["master"]

            opts["proto"] = "manual"
            iface_data[addrfam]["master"] = adapters[iface]["master"]
            iface_data[addrfam]["addrfam"] = addrfam

        elif iface_type == "vlan":
            iface_data[addrfam]["vlan_raw_device"] = re.sub(r"\.\d*", "", iface)
            iface_data[addrfam]["addrfam"] = addrfam

        elif iface_type == "pppoe":
            tmp_ethtool = _parse_ethtool_pppoe_opts(opts, iface)
            if tmp_ethtool:
                for item in tmp_ethtool:
                    adapters[iface]["data"][addrfam][
                        _DEB_CONFIG_PPPOE_OPTS[item]
                    ] = tmp_ethtool[item]
            iface_data[addrfam]["addrfam"] = addrfam

    opts.pop("mode", None)

    for opt, val in opts.items():
        inet = None
        if opt.startswith("ipv4"):
            opt = opt[4:]
            inet = "inet"
            iface_data["inet"]["addrfam"] = "inet"
        elif opt.startswith("ipv6"):
            iface_data["inet6"]["addrfam"] = "inet6"
            opt = opt[4:]
            inet = "inet6"
        elif opt in [
            "ipaddr",
            "address",
            "ipaddresses",
            "addresses",
            "gateway",
            "proto",
        ]:
            iface_data["inet"]["addrfam"] = "inet"
            inet = "inet"

        _opt = SALT_ATTR_TO_DEBIAN_ATTR_MAP.get(opt, opt)
        _debopt = _opt.replace("-", "_")

        for addrfam in ["inet", "inet6"]:
            (valid, value, errmsg) = _validate_interface_option(
                _opt, val, addrfam=addrfam
            )
            if not valid:
                continue
            if inet is None and _debopt not in iface_data[addrfam]:
                iface_data[addrfam][_debopt] = value
            elif inet == addrfam:
                iface_data[addrfam][_debopt] = value

    for opt in [
        "up_cmds",
        "pre_up_cmds",
        "post_up_cmds",
        "down_cmds",
        "pre_down_cmds",
        "post_down_cmds",
    ]:
        if opt in opts:
            iface_data["inet"][opt] = opts[opt]
            iface_data["inet6"][opt] = opts[opt]

    # Remove incomplete/disabled inet blocks
    for (addrfam, opt) in [("inet", "enable_ipv4"), ("inet6", "enable_ipv6")]:
        if opts.get(opt, None) is False:
            iface_data.pop(addrfam)
        elif iface_data[addrfam].get("addrfam", "") != addrfam:
            iface_data.pop(addrfam)

    return adapters


def _parse_settings_source(opts, iface_type, enabled, iface):
    """
    Filters given options and outputs valid settings for a
    network interface.
    """
    adapters = salt.utils.odict.OrderedDict()
    adapters[iface] = salt.utils.odict.OrderedDict()

    adapters[iface]["type"] = iface_type

    adapters[iface]["data"] = salt.utils.odict.OrderedDict()
    iface_data = adapters[iface]["data"]
    iface_data["sources"] = [opts["source"]]

    return adapters


def _parse_network_settings(opts, current):
    """
    Filters given options and outputs valid settings for
    the global network settings file.
    """
    # Normalize keys
    opts = {k.lower(): v for (k, v) in opts.items()}
    current = {k.lower(): v for (k, v) in current.items()}
    result = {}

    valid = _CONFIG_TRUE + _CONFIG_FALSE
    if "enabled" not in opts:
        try:
            opts["networking"] = current["networking"]
            _log_default_network("networking", current["networking"])
        except ValueError:
            _raise_error_network("networking", valid)
    else:
        opts["networking"] = opts["enabled"]

    if opts["networking"] in valid:
        if opts["networking"] in _CONFIG_TRUE:
            result["networking"] = "yes"
        elif opts["networking"] in _CONFIG_FALSE:
            result["networking"] = "no"
    else:
        _raise_error_network("networking", valid)

    if "hostname" not in opts:
        try:
            opts["hostname"] = current["hostname"]
            _log_default_network("hostname", current["hostname"])
        except ValueError:
            _raise_error_network("hostname", ["server1.example.com"])

    if opts["hostname"]:
        result["hostname"] = opts["hostname"]
    else:
        _raise_error_network("hostname", ["server1.example.com"])

    if "search" in opts:
        result["search"] = opts["search"]

    return result


def _parse_routes(iface, opts):
    """
    Filters given options and outputs valid settings for
    the route settings file.
    """
    # Normalize keys
    opts = {k.lower(): v for (k, v) in opts.items()}
    result = {}
    if "routes" not in opts:
        _raise_error_routes(iface, "routes", "List of routes")

    for opt in opts:
        result[opt] = opts[opt]

    return result


def _write_file(iface, data, folder, pattern):
    """
    Writes a file to disk
    """
    filename = os.path.join(folder, pattern.format(iface))
    if not os.path.exists(folder):
        msg = "{0} cannot be written. {1} does not exist"
        msg = msg.format(filename, folder)
        log.error(msg)
        raise AttributeError(msg)
    with salt.utils.files.flopen(filename, "w") as fout:
        fout.write(salt.utils.stringutils.to_str(data))
    return filename


def _write_file_routes(iface, data, folder, pattern):
    """
    Writes a file to disk
    """
    # ifup / ifdown is executing given folder via run-parts.
    # according to run-parts man-page, only filenames with this pattern are
    # executed: (^[a-zA-Z0-9_-]+$)

    # In order to make the routes file work for vlan interfaces
    # (default would have been in example /etc/network/if-up.d/route-bond0.12)
    # these dots in the iface name need to be replaced by underscores, so it
    # can be executed by run-parts
    iface = iface.replace(".", "_")

    filename = os.path.join(folder, pattern.format(iface))
    if not os.path.exists(folder):
        msg = "{0} cannot be written. {1} does not exist"
        msg = msg.format(filename, folder)
        log.error(msg)
        raise AttributeError(msg)
    with salt.utils.files.flopen(filename, "w") as fout:
        fout.write(salt.utils.stringutils.to_str(data))

    __salt__["file.set_mode"](filename, "0755")
    return filename


def _write_file_network(data, filename, create=False):
    """
    Writes a file to disk
    If file does not exist, only create if create
    argument is True
    """
    if not os.path.exists(filename) and not create:
        msg = "{0} cannot be written. {0} does not exist\
                and create is set to False"
        msg = msg.format(filename)
        log.error(msg)
        raise AttributeError(msg)
    with salt.utils.files.flopen(filename, "w") as fout:
        fout.write(salt.utils.stringutils.to_str(data))


def _read_temp(data):
    """
    Return what would be written to disk
    """
    tout = StringIO()
    tout.write(data)
    tout.seek(0)
    output = tout.readlines()
    tout.close()

    return output


def _read_temp_ifaces(iface, data):
    """
    Return what would be written to disk for interfaces
    """
    try:
        template = JINJA.get_template("debian_eth.jinja")
    except jinja2.exceptions.TemplateNotFound:
        log.error("Could not load template debian_eth.jinja")
        return ""

    ifcfg = template.render({"name": iface, "data": data})
    # Return as an array so the difflib works
    return [item + "\n" for item in ifcfg.split("\n")]


def _write_file_ifaces(iface, data, **settings):
    """
    Writes a file to disk
    """
    try:
        eth_template = JINJA.get_template("debian_eth.jinja")
        source_template = JINJA.get_template("debian_source.jinja")
    except jinja2.exceptions.TemplateNotFound:
        log.error("Could not load template debian_eth.jinja")
        return ""

    # Read /etc/network/interfaces into a dict
    adapters = _parse_interfaces()
    # Apply supplied settings over on-disk settings
    adapters[iface] = data

    ifcfg = ""
    for adapter in adapters:
        if "type" in adapters[adapter] and adapters[adapter]["type"] == "source":
            tmp = source_template.render({"name": adapter, "data": adapters[adapter]})
        else:
            tmp = eth_template.render({"name": adapter, "data": adapters[adapter]})
        ifcfg = ifcfg + tmp
        if adapter == iface:
            saved_ifcfg = tmp

    _SEPARATE_FILE = False
    if "filename" in settings:
        if not settings["filename"].startswith("/"):
            filename = "{}/{}".format(_DEB_NETWORK_DIR, settings["filename"])
        else:
            filename = settings["filename"]
        _SEPARATE_FILE = True
    else:
        if "filename" in adapters[adapter]["data"]:
            filename = adapters[adapter]["data"]
        else:
            filename = _DEB_NETWORK_FILE

    if not os.path.exists(os.path.dirname(filename)):
        msg = "{0} cannot be written."
        msg = msg.format(os.path.dirname(filename))
        log.error(msg)
        raise AttributeError(msg)
    with salt.utils.files.flopen(filename, "w") as fout:
        if _SEPARATE_FILE:
            fout.write(salt.utils.stringutils.to_str(saved_ifcfg))
        else:
            fout.write(salt.utils.stringutils.to_str(ifcfg))

    # Return as an array so the difflib works
    return saved_ifcfg.split("\n")


def _write_file_ppp_ifaces(iface, data):
    """
    Writes a file to disk
    """
    try:
        template = JINJA.get_template("debian_ppp_eth.jinja")
    except jinja2.exceptions.TemplateNotFound:
        log.error("Could not load template debian_ppp_eth.jinja")
        return ""

    adapters = _parse_interfaces()
    adapters[iface] = data

    ifcfg = ""
    tmp = template.render({"data": adapters[iface]})
    ifcfg = tmp + ifcfg

    filename = _DEB_PPP_DIR + "/" + adapters[iface]["data"]["inet"]["provider"]
    if not os.path.exists(os.path.dirname(filename)):
        msg = "{0} cannot be written."
        msg = msg.format(os.path.dirname(filename))
        log.error(msg)
        raise AttributeError(msg)
    with salt.utils.files.fopen(filename, "w") as fout:
        fout.write(salt.utils.stringutils.to_str(ifcfg))

    # Return as an array so the difflib works
    return filename


def build_bond(iface, **settings):
    """
    Create a bond script in /etc/modprobe.d with the passed settings
    and load the bonding kernel module.

    CLI Example:

    .. code-block:: bash

        salt '*' ip.build_bond bond0 mode=balance-alb
    """
    deb_major = __grains__["osrelease"][:1]

    opts = _parse_settings_bond(settings, iface)
    try:
        template = JINJA.get_template("conf.jinja")
    except jinja2.exceptions.TemplateNotFound:
        log.error("Could not load template conf.jinja")
        return ""
    data = template.render({"name": iface, "bonding": opts})

    if "test" in settings and settings["test"]:
        return _read_temp(data)

    _write_file(iface, data, _DEB_NETWORK_CONF_FILES, "{}.conf".format(iface))
    path = os.path.join(_DEB_NETWORK_CONF_FILES, "{}.conf".format(iface))
    if deb_major == "5":
        for line_type in ("alias", "options"):
            cmd = [
                "sed",
                "-i",
                "-e",
                r"/^{}\s{}.*/d".format(line_type, iface),
                "/etc/modprobe.conf",
            ]
            __salt__["cmd.run"](cmd, python_shell=False)
        __salt__["file.append"]("/etc/modprobe.conf", path)

    # Load kernel module
    __salt__["kmod.load"]("bonding")

    # install ifenslave-2.6
    __salt__["pkg.install"]("ifenslave-2.6")

    return _read_file(path)


def build_interface(iface, iface_type, enabled, **settings):
    """
    Build an interface script for a network interface.

    CLI Example:

    .. code-block:: bash

        salt '*' ip.build_interface eth0 eth <settings>
    """

    iface_type = iface_type.lower()

    if iface_type not in _IFACE_TYPES:
        _raise_error_iface(iface, iface_type, _IFACE_TYPES)

    if iface_type == "slave":
        settings["slave"] = "yes"
        if "master" not in settings:
            msg = "master is a required setting for slave interfaces"
            log.error(msg)
            raise AttributeError(msg)

    elif iface_type == "vlan":
        settings["vlan"] = "yes"
        __salt__["pkg.install"]("vlan")

    elif iface_type == "pppoe":
        settings["pppoe"] = "yes"
        if not __salt__["pkg.version"]("ppp"):
            inst = __salt__["pkg.install"]("ppp")

    elif iface_type == "bond":
        if "slaves" not in settings:
            msg = "slaves is a required setting for bond interfaces"
            log.error(msg)
            raise AttributeError(msg)

    elif iface_type == "bridge":
        if "ports" not in settings:
            msg = (
                "ports is a required setting for bridge interfaces on Debian "
                "or Ubuntu based systems"
            )
            log.error(msg)
            raise AttributeError(msg)
        __salt__["pkg.install"]("bridge-utils")

    if iface_type in ["eth", "bond", "bridge", "slave", "vlan", "pppoe"]:
        opts = _parse_settings_eth(settings, iface_type, enabled, iface)

    if iface_type in ["source"]:
        opts = _parse_settings_source(settings, iface_type, enabled, iface)

    if "test" in settings and settings["test"]:
        return _read_temp_ifaces(iface, opts[iface])

    ifcfg = _write_file_ifaces(iface, opts[iface], **settings)

    if iface_type == "pppoe":
        _write_file_ppp_ifaces(iface, opts[iface])

    # ensure lines in list end with newline, so difflib works
    return [item + "\n" for item in ifcfg]


def build_routes(iface, **settings):
    """
    Add route scripts for a network interface using up commands.

    CLI Example:

    .. code-block:: bash

        salt '*' ip.build_routes eth0 <settings>
    """

    opts = _parse_routes(iface, settings)
    try:
        template = JINJA.get_template("route_eth.jinja")
    except jinja2.exceptions.TemplateNotFound:
        log.error("Could not load template route_eth.jinja")
        return ""

    add_routecfg = template.render(route_type="add", routes=opts["routes"], iface=iface)

    del_routecfg = template.render(route_type="del", routes=opts["routes"], iface=iface)

    if "test" in settings and settings["test"]:
        return _read_temp(add_routecfg + del_routecfg)

    filename = _write_file_routes(iface, add_routecfg, _DEB_NETWORK_UP_DIR, "route-{0}")
    results = _read_file(filename)

    filename = _write_file_routes(
        iface, del_routecfg, _DEB_NETWORK_DOWN_DIR, "route-{0}"
    )
    results += _read_file(filename)

    return results


def down(iface, iface_type):
    """
    Shutdown a network interface

    CLI Example:

    .. code-block:: bash

        salt '*' ip.down eth0 eth
    """
    # Slave devices are controlled by the master.
    # Source 'interfaces' aren't brought down.
    if iface_type not in ["slave", "source"]:
        return __salt__["cmd.run"](["ifdown", iface])
    return None


def get_bond(iface):
    """
    Return the content of a bond script

    CLI Example:

    .. code-block:: bash

        salt '*' ip.get_bond bond0
    """
    path = os.path.join(_DEB_NETWORK_CONF_FILES, "{}.conf".format(iface))
    return _read_file(path)


def get_interface(iface):
    """
    Return the contents of an interface script

    CLI Example:

    .. code-block:: bash

        salt '*' ip.get_interface eth0
    """

    adapters = _parse_interfaces()
    if iface in adapters:
        try:
            if iface == "source":
                template = JINJA.get_template("debian_source.jinja")
            else:
                template = JINJA.get_template("debian_eth.jinja")
        except jinja2.exceptions.TemplateNotFound:
            log.error("Could not load template debian_eth.jinja")
            return ""

        ifcfg = template.render({"name": iface, "data": adapters[iface]})

        # ensure lines in list end with newline, so difflib works
        return [item + "\n" for item in ifcfg.split("\n")]
    else:
        return []


def up(iface, iface_type):  # pylint: disable=C0103
    """
    Start up a network interface

    CLI Example:

    .. code-block:: bash

        salt '*' ip.up eth0 eth
    """
    # Slave devices are controlled by the master.
    # Source 'interfaces' aren't brought up.
    if iface_type not in ("slave", "source"):
        return __salt__["cmd.run"](["ifup", iface])
    return None


def get_network_settings():
    """
    Return the contents of the global network script.

    CLI Example:

    .. code-block:: bash

        salt '*' ip.get_network_settings
    """
    skip_etc_default_networking = (
        __grains__["osfullname"] == "Ubuntu"
        and int(__grains__["osrelease"].split(".")[0]) >= 12
    )

    if skip_etc_default_networking:
        settings = {}
        if __salt__["service.available"]("networking"):
            if __salt__["service.status"]("networking"):
                settings["networking"] = "yes"
            else:
                settings["networking"] = "no"
        else:
            settings["networking"] = "no"

        hostname = _parse_hostname()
        domainname = _parse_domainname()

        settings["hostname"] = hostname
        settings["domainname"] = domainname

    else:
        settings = _parse_current_network_settings()

    try:
        template = JINJA.get_template("display-network.jinja")
    except jinja2.exceptions.TemplateNotFound:
        log.error("Could not load template display-network.jinja")
        return ""

    network = template.render(settings)
    return _read_temp(network)


def get_routes(iface):
    """
    Return the routes for the interface

    CLI Example:

    .. code-block:: bash

        salt '*' ip.get_routes eth0
    """

    filename = os.path.join(_DEB_NETWORK_UP_DIR, "route-{}".format(iface))
    results = _read_file(filename)

    filename = os.path.join(_DEB_NETWORK_DOWN_DIR, "route-{}".format(iface))
    results += _read_file(filename)

    return results


def apply_network_settings(**settings):
    """
    Apply global network configuration.

    CLI Example:

    .. code-block:: bash

        salt '*' ip.apply_network_settings
    """
    if "require_reboot" not in settings:
        settings["require_reboot"] = False

    if "apply_hostname" not in settings:
        settings["apply_hostname"] = False

    hostname_res = True
    if settings["apply_hostname"] in _CONFIG_TRUE:
        if "hostname" in settings:
            hostname_res = __salt__["network.mod_hostname"](settings["hostname"])
        else:
            log.warning(
                "The network state sls is trying to apply hostname "
                "changes but no hostname is defined."
            )
            hostname_res = False

    res = True
    if settings["require_reboot"] in _CONFIG_TRUE:
        log.warning(
            "The network state sls is requiring a reboot of the system to "
            "properly apply network configuration."
        )
        res = True
    else:
        stop = __salt__["service.stop"]("networking")
        time.sleep(2)
        res = stop and __salt__["service.start"]("networking")

    return hostname_res and res


def build_network_settings(**settings):
    """
    Build the global network script.

    CLI Example:

    .. code-block:: bash

        salt '*' ip.build_network_settings <settings>
    """
    changes = []

    # Read current configuration and store default values
    current_network_settings = _parse_current_network_settings()

    # Build settings
    opts = _parse_network_settings(settings, current_network_settings)

    # Ubuntu has moved away from /etc/default/networking
    # beginning with the 12.04 release so we disable or enable
    # the networking related services on boot
    skip_etc_default_networking = (
        __grains__["osfullname"] == "Ubuntu"
        and int(__grains__["osrelease"].split(".")[0]) >= 12
    )
    if skip_etc_default_networking:
        if opts["networking"] == "yes":
            service_cmd = "service.enable"
        else:
            service_cmd = "service.disable"

        if __salt__["service.available"]("NetworkManager"):
            __salt__[service_cmd]("NetworkManager")

        if __salt__["service.available"]("networking"):
            __salt__[service_cmd]("networking")
    else:
        try:
            template = JINJA.get_template("network.jinja")
        except jinja2.exceptions.TemplateNotFound:
            log.error("Could not load template network.jinja")
            return ""
        network = template.render(opts)

        if "test" in settings and settings["test"]:
            return _read_temp(network)
        # Write settings
        _write_file_network(network, _DEB_NETWORKING_FILE, True)

    # Get hostname and domain from opts
    sline = opts["hostname"].split(".", 1)
    opts["hostname"] = sline[0]
    current_domainname = current_network_settings["domainname"]
    current_searchdomain = current_network_settings["searchdomain"]

    new_domain = False
    if len(sline) > 1:
        new_domainname = sline[1]
        if new_domainname != current_domainname:
            domainname = new_domainname
            opts["domainname"] = new_domainname
            new_domain = True
        else:
            domainname = current_domainname
            opts["domainname"] = domainname
    else:
        domainname = current_domainname
        opts["domainname"] = domainname

    new_search = False
    if "search" in opts:
        new_searchdomain = opts["search"]
        if new_searchdomain != current_searchdomain:
            searchdomain = new_searchdomain
            opts["searchdomain"] = new_searchdomain
            new_search = True
        else:
            searchdomain = current_searchdomain
            opts["searchdomain"] = searchdomain
    else:
        searchdomain = current_searchdomain
        opts["searchdomain"] = searchdomain

    # If the domain changes, then we should write the resolv.conf file.
    if new_domain or new_search:
        # Look for existing domain line and update if necessary
        resolve = _parse_resolve()
        domain_prog = re.compile(r"domain\s+")
        search_prog = re.compile(r"search\s+")
        new_contents = []

        for item in _read_file(_DEB_RESOLV_FILE):
            if domain_prog.match(item):
                item = "domain {}".format(domainname)
            elif search_prog.match(item):
                item = "search {}".format(searchdomain)
            new_contents.append(item)

        # A domain line didn't exist so we'll add one in
        # with the new domainname
        if "domain" not in resolve:
            new_contents.insert(0, "domain {}".format(domainname))

        # A search line didn't exist so we'll add one in
        # with the new search domain
        if "search" not in resolve:
            new_contents.insert("domain" in resolve, "search {}".format(searchdomain))

        new_resolv = "\n".join(new_contents)

        # Write /etc/resolv.conf
        if not ("test" in settings and settings["test"]):
            _write_file_network(new_resolv, _DEB_RESOLV_FILE)

    #  used for returning the results back
    try:
        template = JINJA.get_template("display-network.jinja")
    except jinja2.exceptions.TemplateNotFound:
        log.error("Could not load template display-network.jinja")
        return ""
    network = template.render(opts)
    changes.extend(_read_temp(network))

    return changes
