From 79754e39f8159f21e2ff1a4fdccf0b1fe053ad5f Mon Sep 17 00:00:00 2001
From: Victor Zhestkov <35733135+vzhestkov@users.noreply.github.com>
Date: Thu, 18 Feb 2021 15:55:32 +0300
Subject: [PATCH] Implementation of suse_ip execution module
 (bsc#1099976) (#299)

* Implementation of suse_ip execution module

* Add suse_ip templates to setup.py
---
 salt/modules/linux_ip.py             |    2 +
 salt/modules/rh_ip.py                |    2 +-
 salt/modules/suse_ip.py              | 1151 ++++++++++++++++++++++++++
 salt/states/network.py               |   33 +-
 salt/templates/suse_ip/ifcfg.jinja   |   34 +
 salt/templates/suse_ip/ifroute.jinja |    8 +
 salt/templates/suse_ip/network.jinja |   30 +
 setup.py                             |    1 +
 8 files changed, 1247 insertions(+), 14 deletions(-)
 create mode 100644 salt/modules/suse_ip.py
 create mode 100644 salt/templates/suse_ip/ifcfg.jinja
 create mode 100644 salt/templates/suse_ip/ifroute.jinja
 create mode 100644 salt/templates/suse_ip/network.jinja

diff --git a/salt/modules/linux_ip.py b/salt/modules/linux_ip.py
index f76497b8ee..4c9b63071a 100644
--- a/salt/modules/linux_ip.py
+++ b/salt/modules/linux_ip.py
@@ -22,6 +22,8 @@ def __virtual__():
     '''
     if salt.utils.platform.is_windows():
         return (False, 'Module linux_ip: Windows systems are not supported.')
+    if __grains__['os_family'] == 'Suse':
+        return (False, 'Module linux_ip: SUSE systems are not supported.')
     if __grains__['os_family'] == 'RedHat':
         return (False, 'Module linux_ip: RedHat systems are not supported.')
     if __grains__['os_family'] == 'Debian':
diff --git a/salt/modules/rh_ip.py b/salt/modules/rh_ip.py
index 85b9d5701a..f3048777ff 100644
--- a/salt/modules/rh_ip.py
+++ b/salt/modules/rh_ip.py
@@ -603,7 +603,7 @@ def _parse_settings_eth(opts, iface_type, enabled, iface):
     '''
     result = {'name': iface}
     if 'proto' in opts:
-        valid = ['none', 'bootp', 'dhcp']
+        valid = ['none', 'static', 'bootp', 'dhcp']
         if opts['proto'] in valid:
             result['proto'] = opts['proto']
         else:
diff --git a/salt/modules/suse_ip.py b/salt/modules/suse_ip.py
new file mode 100644
index 0000000000..92dad50351
--- /dev/null
+++ b/salt/modules/suse_ip.py
@@ -0,0 +1,1151 @@
+# -*- coding: utf-8 -*-
+"""
+The networking module for SUSE based distros
+"""
+from __future__ import absolute_import, print_function, unicode_literals
+
+# Import python libs
+import logging
+import os
+
+# Import third party libs
+import jinja2
+import jinja2.exceptions
+
+# Import salt libs
+import salt.utils.files
+import salt.utils.stringutils
+import salt.utils.templates
+import salt.utils.validate.net
+from salt.exceptions import CommandExecutionError
+from salt.ext import six
+
+# 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, "suse_ip")
+    )
+)
+
+# Define the module's virtual name
+__virtualname__ = "ip"
+
+# Default values for bonding
+_BOND_DEFAULTS = {
+    # 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",
+    # 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",
+    # Used with miimon.
+    # On: driver sends mii
+    # Off: ethtool sends mii
+    "use_carrier": "0",
+    # Default. Don't change unless you know what you are doing.
+    "xmit_hash_policy": "layer2",
+}
+_SUSE_NETWORK_SCRIPT_DIR = "/etc/sysconfig/network"
+_SUSE_NETWORK_FILE = "/etc/sysconfig/network/config"
+_SUSE_NETWORK_ROUTES_FILE = "/etc/sysconfig/network/routes"
+_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",
+    "ipip",
+    "ib",
+)
+
+
+def __virtual__():
+    """
+    Confine this module to SUSE based distros
+    """
+    if __grains__["os_family"] == "Suse":
+        return __virtualname__
+    return (
+        False,
+        "The suse_ip execution module cannot be loaded: this module is only available on SUSE based distributions.",
+    )
+
+
+def _error_msg_iface(iface, option, expected):
+    """
+    Build an appropriate error message from a given option and
+    a list of expected values.
+    """
+    if isinstance(expected, six.string_types):
+        expected = (expected,)
+    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.
+    """
+    if isinstance(expected, six.string_types):
+        expected = (expected,)
+    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 _parse_suse_config(path):
+    suse_config = _read_file(path)
+    cv_suse_config = {}
+    if suse_config:
+        for line in suse_config:
+            line = line.strip()
+            if len(line) == 0 or line.startswith("!") or line.startswith("#"):
+                continue
+            pair = [p.rstrip() for p in line.split("=", 1)]
+            if len(pair) != 2:
+                continue
+            name, value = pair
+            cv_suse_config[name.upper()] = salt.utils.stringutils.dequote(value)
+
+    return cv_suse_config
+
+
+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 six.text_type(opts["speed"]) in valid:
+            config.update({"speed": opts["speed"]})
+        else:
+            _raise_error_iface(iface, opts["speed"], valid)
+
+    if "advertise" in opts:
+        valid = [
+            "0x001",
+            "0x002",
+            "0x004",
+            "0x008",
+            "0x010",
+            "0x020",
+            "0x20000",
+            "0x8000",
+            "0x1000",
+            "0x40000",
+            "0x80000",
+            "0x200000",
+            "0x400000",
+            "0x800000",
+            "0x1000000",
+            "0x2000000",
+            "0x4000000",
+        ]
+        if six.text_type(opts["advertise"]) in valid:
+            config.update({"advertise": opts["advertise"]})
+        else:
+            _raise_error_iface(iface, "advertise", 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_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.
+    """
+    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)
+    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)
+    elif opts["mode"] in ("balance-xor", "2"):
+        log.info("Device: %s Bonding Mode: load balancing (xor)", iface)
+        return _parse_settings_bond_2(opts, iface)
+    elif opts["mode"] in ("broadcast", "3"):
+        log.info("Device: %s Bonding Mode: fault-tolerance (broadcast)", iface)
+        return _parse_settings_bond_3(opts, iface)
+    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)
+    elif opts["mode"] in ("balance-tlb", "5"):
+        log.info("Device: %s Bonding Mode: transmit load balancing", iface)
+        return _parse_settings_bond_5(opts, iface)
+    elif opts["mode"] in ("balance-alb", "6"):
+        log.info("Device: %s Bonding Mode: adaptive load balancing", iface)
+        return _parse_settings_bond_6(opts, iface)
+    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_miimon(opts, iface):
+    """
+    Add shared settings for miimon support used by balance-rr, balance-xor
+    bonding types.
+    """
+    ret = {}
+    for binding in ("miimon", "downdelay", "updelay"):
+        if binding in opts:
+            try:
+                int(opts[binding])
+                ret.update({binding: opts[binding]})
+            except Exception:  # pylint: disable=broad-except
+                _raise_error_iface(iface, binding, "integer")
+
+    if "miimon" in opts:
+        if not opts["miimon"]:
+            _raise_error_iface(iface, "miimon", "nonzero integer")
+
+        for binding in ("downdelay", "updelay"):
+            if binding in ret:
+                if ret[binding] % ret["miimon"]:
+                    _raise_error_iface(
+                        iface,
+                        binding,
+                        "0 or a multiple of miimon ({0})".format(ret["miimon"]),
+                    )
+
+        if "use_carrier" in opts:
+            if opts["use_carrier"] in _CONFIG_TRUE:
+                ret.update({"use_carrier": "1"})
+            elif opts["use_carrier"] in _CONFIG_FALSE:
+                ret.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_DEFAULTS["use_carrier"])
+            ret.update({"use_carrier": _BOND_DEFAULTS["use_carrier"]})
+
+    return ret
+
+
+def _parse_settings_arp(opts, iface):
+    """
+    Add shared settings for arp used by balance-rr, balance-xor bonding types.
+    """
+    ret = {}
+    if "arp_interval" in opts:
+        try:
+            int(opts["arp_interval"])
+            ret.update({"arp_interval": opts["arp_interval"]})
+        except Exception:  # pylint: disable=broad-except
+            _raise_error_iface(iface, "arp_interval", "integer")
+
+        # 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:
+                    ret.update({"arp_ip_target": ",".join(opts["arp_ip_target"])})
+                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)
+
+    return ret
+
+
+def _parse_settings_bond_0(opts, iface):
+    """
+    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"}
+    bond.update(_parse_settings_miimon(opts, iface))
+    bond.update(_parse_settings_arp(opts, iface))
+
+    if "miimon" not in opts and "arp_interval" not in opts:
+        _raise_error_iface(
+            iface, "miimon or arp_interval", "at least one of these is required"
+        )
+
+    return bond
+
+
+def _parse_settings_bond_1(opts, iface):
+
+    """
+    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"}
+    bond.update(_parse_settings_miimon(opts, iface))
+
+    if "miimon" not in opts:
+        _raise_error_iface(iface, "miimon", "integer")
+
+    if "primary" in opts:
+        bond.update({"primary": opts["primary"]})
+
+    return bond
+
+
+def _parse_settings_bond_2(opts, iface):
+    """
+    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"}
+    bond.update(_parse_settings_miimon(opts, iface))
+    bond.update(_parse_settings_arp(opts, iface))
+
+    if "miimon" not in opts and "arp_interval" not in opts:
+        _raise_error_iface(
+            iface, "miimon or arp_interval", "at least one of these is required"
+        )
+
+    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):
+
+    """
+    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"}
+    bond.update(_parse_settings_miimon(opts, iface))
+
+    if "miimon" not in opts:
+        _raise_error_iface(iface, "miimon", "integer")
+
+    return bond
+
+
+def _parse_settings_bond_4(opts, iface):
+    """
+    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"}
+    bond.update(_parse_settings_miimon(opts, iface))
+
+    if "miimon" not in opts:
+        _raise_error_iface(iface, "miimon", "integer")
+
+    for binding in ("lacp_rate", "ad_select"):
+        if binding in opts:
+            if binding == "lacp_rate":
+                valid = ("fast", "1", "slow", "0")
+                if opts[binding] not in valid:
+                    _raise_error_iface(iface, binding, valid)
+                if opts[binding] == "fast":
+                    opts.update({binding: "1"})
+                if opts[binding] == "slow":
+                    opts.update({binding: "0"})
+            else:
+                valid = "integer"
+            try:
+                int(opts[binding])
+                bond.update({binding: opts[binding]})
+            except Exception:  # pylint: disable=broad-except
+                _raise_error_iface(iface, binding, valid)
+        else:
+            _log_default_iface(iface, binding, _BOND_DEFAULTS[binding])
+            bond.update({binding: _BOND_DEFAULTS[binding]})
+
+    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):
+
+    """
+    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"}
+    bond.update(_parse_settings_miimon(opts, iface))
+
+    if "miimon" not in opts:
+        _raise_error_iface(iface, "miimon", "integer")
+
+    if "primary" in opts:
+        bond.update({"primary": opts["primary"]})
+
+    return bond
+
+
+def _parse_settings_bond_6(opts, iface):
+
+    """
+    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"}
+    bond.update(_parse_settings_miimon(opts, iface))
+
+    if "miimon" not in opts:
+        _raise_error_iface(iface, "miimon", "integer")
+
+    if "primary" in opts:
+        bond.update({"primary": opts["primary"]})
+
+    return bond
+
+
+def _parse_settings_vlan(opts, iface):
+
+    """
+    Filters given options and outputs valid settings for a vlan
+    """
+    vlan = {}
+    if "reorder_hdr" in opts:
+        if opts["reorder_hdr"] in _CONFIG_TRUE + _CONFIG_FALSE:
+            vlan.update({"reorder_hdr": opts["reorder_hdr"]})
+        else:
+            valid = _CONFIG_TRUE + _CONFIG_FALSE
+            _raise_error_iface(iface, "reorder_hdr", valid)
+
+    if "vlan_id" in opts:
+        if opts["vlan_id"] > 0:
+            vlan.update({"vlan_id": opts["vlan_id"]})
+        else:
+            _raise_error_iface(iface, "vlan_id", "Positive integer")
+
+    if "phys_dev" in opts:
+        if len(opts["phys_dev"]) > 0:
+            vlan.update({"phys_dev": opts["phys_dev"]})
+        else:
+            _raise_error_iface(iface, "phys_dev", "Non-empty string")
+
+    return vlan
+
+
+def _parse_settings_eth(opts, iface_type, enabled, iface):
+    """
+    Filters given options and outputs valid settings for a
+    network interface.
+    """
+    result = {"name": iface}
+    if "proto" in opts:
+        valid = ["static", "dhcp", "dhcp4", "dhcp6", "autoip", "dhcp+autoip", "auto6", "6to4", "none"]
+        if opts["proto"] in valid:
+            result["proto"] = opts["proto"]
+        else:
+            _raise_error_iface(iface, opts["proto"], valid)
+
+    if "mtu" in opts:
+        try:
+            result["mtu"] = int(opts["mtu"])
+        except ValueError:
+            _raise_error_iface(iface, "mtu", ["integer"])
+
+    if "hwaddr" in opts and "macaddr" in opts:
+        msg = "Cannot pass both hwaddr and macaddr. Must use either hwaddr or macaddr"
+        log.error(msg)
+        raise AttributeError(msg)
+
+    if iface_type not in ("bridge",):
+        ethtool = _parse_ethtool_opts(opts, iface)
+        if ethtool:
+            result["ethtool"] = " ".join(
+                ["{0} {1}".format(x, y) for x, y in ethtool.items()]
+            )
+
+    if iface_type == "slave":
+        result["proto"] = "none"
+
+
+    if iface_type == "bond":
+        if "mode" not in opts:
+            msg = "Missing required option 'mode'"
+            log.error("%s for bond interface '%s'", msg, iface)
+            raise AttributeError(msg)
+        bonding = _parse_settings_bond(opts, iface)
+        if bonding:
+            result["bonding"] = " ".join(
+                ["{0}={1}".format(x, y) for x, y in bonding.items()]
+            )
+            result["devtype"] = "Bond"
+            if "slaves" in opts:
+                if isinstance(opts["slaves"], list):
+                    result["slaves"] = opts["slaves"]
+                else:
+                    result["slaves"] = opts["slaves"].split()
+
+    if iface_type == "vlan":
+        vlan = _parse_settings_vlan(opts, iface)
+        if vlan:
+            result["devtype"] = "Vlan"
+            for opt in vlan:
+                result[opt] = opts[opt]
+
+    if iface_type == "eth":
+        result["devtype"] = "Ethernet"
+
+    if iface_type == "bridge":
+        result["devtype"] = "Bridge"
+        bypassfirewall = True
+        valid = _CONFIG_TRUE + _CONFIG_FALSE
+        for opt in ("bypassfirewall",):
+            if opt in opts:
+                if opts[opt] in _CONFIG_TRUE:
+                    bypassfirewall = True
+                elif opts[opt] in _CONFIG_FALSE:
+                    bypassfirewall = False
+                else:
+                    _raise_error_iface(iface, opts[opt], valid)
+
+        bridgectls = [
+            "net.bridge.bridge-nf-call-ip6tables",
+            "net.bridge.bridge-nf-call-iptables",
+            "net.bridge.bridge-nf-call-arptables",
+        ]
+
+        if bypassfirewall:
+            sysctl_value = 0
+        else:
+            sysctl_value = 1
+
+        for sysctl in bridgectls:
+            try:
+                __salt__["sysctl.persist"](sysctl, sysctl_value)
+            except CommandExecutionError:
+                log.warning("Failed to set sysctl: %s", sysctl)
+
+    else:
+        if "bridge" in opts:
+            result["bridge"] = opts["bridge"]
+
+    if iface_type == "ipip":
+        result["devtype"] = "IPIP"
+        for opt in ("my_inner_ipaddr", "my_outer_ipaddr"):
+            if opt not in opts:
+                _raise_error_iface(iface, opt, "1.2.3.4")
+            else:
+                result[opt] = opts[opt]
+    if iface_type == "ib":
+        result["devtype"] = "InfiniBand"
+
+    if "prefix" in opts:
+        if "netmask" in opts:
+            msg = "Cannot use prefix and netmask together"
+            log.error(msg)
+            raise AttributeError(msg)
+        result["prefix"] = opts["prefix"]
+    elif "netmask" in opts:
+        result["netmask"] = opts["netmask"]
+
+    for opt in (
+        "ipaddr",
+        "master",
+        "srcaddr",
+        "delay",
+        "domain",
+        "gateway",
+        "uuid",
+        "nickname",
+        "zone",
+    ):
+        if opt in opts:
+            result[opt] = opts[opt]
+
+    if "ipaddrs" in opts or "ipv6addr" in opts or "ipv6addrs" in opts:
+        result["ipaddrs"] = []
+        addrs = list
+        for opt in opts["ipaddrs"]:
+            if salt.utils.validate.net.ipv4_addr(opt) or salt.utils.validate.net.ipv6_addr(opt):
+                result['ipaddrs'].append(opt)
+            else:
+                msg = "{0} is invalid ipv4 or ipv6 CIDR"
+                log.error(msg)
+                raise AttributeError(msg)
+        if salt.utils.validate.net.ipv6_addr(opts["ipv6addr"]):
+            result['ipaddrs'].append(opts["ipv6addr"])
+        else:
+            msg = "{0} is invalid ipv6 CIDR"
+            log.error(msg)
+            raise AttributeError(msg)
+        for opt in opts["ipv6addrs"]:
+            if salt.utils.validate.net.ipv6_addr(opt):
+                result['ipaddrs'].append(opt)
+            else:
+                msg = "{0} is invalid ipv6 CIDR"
+                log.error(msg)
+                raise AttributeError(msg)
+
+    if "enable_ipv6" in opts:
+        result["enable_ipv6"] = opts["enable_ipv6"]
+
+    valid = _CONFIG_TRUE + _CONFIG_FALSE
+    for opt in (
+        "onparent",
+        "peerdns",
+        "peerroutes",
+        "slave",
+        "vlan",
+        "defroute",
+        "stp",
+        "ipv6_peerdns",
+        "ipv6_defroute",
+        "ipv6_peerroutes",
+        "ipv6_autoconf",
+        "ipv4_failure_fatal",
+        "dhcpv6c",
+    ):
+        if opt in opts:
+            if opts[opt] in _CONFIG_TRUE:
+                result[opt] = "yes"
+            elif opts[opt] in _CONFIG_FALSE:
+                result[opt] = "no"
+            else:
+                _raise_error_iface(iface, opts[opt], valid)
+
+    if "onboot" in opts:
+        log.warning(
+            "The 'onboot' option is controlled by the 'enabled' option. "
+            "Interface: %s Enabled: %s",
+            iface,
+            enabled,
+        )
+
+    if "startmode" in opts:
+        valid = ("manual", "auto", "nfsroot", "hotplug", "off")
+        if opts["startmode"] in valid:
+            result["startmode"] = opts["startmode"]
+        else:
+            _raise_error_iface(iface, opts["startmode"], valid)
+    else:
+        if enabled:
+            result["startmode"] = "auto"
+        else:
+            result["startmode"] = "off"
+
+    # This vlan is in opts, and should be only used in range interface
+    # will affect jinja template for interface generating
+    if "vlan" in opts:
+        if opts["vlan"] in _CONFIG_TRUE:
+            result["vlan"] = "yes"
+        elif opts["vlan"] in _CONFIG_FALSE:
+            result["vlan"] = "no"
+        else:
+            _raise_error_iface(iface, opts["vlan"], valid)
+
+    if "arpcheck" in opts:
+        if opts["arpcheck"] in _CONFIG_FALSE:
+            result["arpcheck"] = "no"
+
+    if "ipaddr_start" in opts:
+        result["ipaddr_start"] = opts["ipaddr_start"]
+
+    if "ipaddr_end" in opts:
+        result["ipaddr_end"] = opts["ipaddr_end"]
+
+    if "clonenum_start" in opts:
+        result["clonenum_start"] = opts["clonenum_start"]
+
+    if "hwaddr" in opts:
+        result["hwaddr"] = opts["hwaddr"]
+
+    if "macaddr" in opts:
+        result["macaddr"] = opts["macaddr"]
+
+    # If NetworkManager is available, we can control whether we use
+    # it or not
+    if "nm_controlled" in opts:
+        if opts["nm_controlled"] in _CONFIG_TRUE:
+            result["nm_controlled"] = "yes"
+        elif opts["nm_controlled"] in _CONFIG_FALSE:
+            result["nm_controlled"] = "no"
+        else:
+            _raise_error_iface(iface, opts["nm_controlled"], valid)
+    else:
+        result["nm_controlled"] = "no"
+
+    return result
+
+
+def _parse_routes(iface, opts):
+    """
+    Filters given options and outputs valid settings for
+    the route settings file.
+    """
+    # Normalize keys
+    opts = dict((k.lower(), v) for (k, v) in six.iteritems(opts))
+    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 _parse_network_settings(opts, current):
+    """
+    Filters given options and outputs valid settings for
+    the global network settings file.
+    """
+    # Normalize keys
+    opts = dict((k.lower(), v) for (k, v) in six.iteritems(opts))
+    current = dict((k.lower(), v) for (k, v) in six.iteritems(current))
+
+    # Check for supported parameters
+    retain_settings = opts.get("retain_settings", False)
+    result = {}
+    if retain_settings:
+        for opt in current:
+            nopt = opt
+            if opt == "netconfig_dns_static_servers":
+                nopt = "dns"
+                result[nopt] = current[opt].split()
+            elif opt == "netconfig_dns_static_searchlist":
+                nopt = "dns_search"
+                result[nopt] = current[opt].split()
+            elif opt.startswith("netconfig_") and opt not in ("netconfig_modules_order", "netconfig_verbose", "netconfig_force_replace"):
+                nopt = opt[10:]
+                result[nopt] = current[opt]
+            else:
+                result[nopt] = current[opt]
+            _log_default_network(nopt, current[opt])
+
+    for opt in opts:
+        if opt in ("dns", "dns_search") and not isinstance(opts[opt], list):
+            result[opt] = opts[opt].split()
+        else:
+            result[opt] = opts[opt]
+    return result
+
+
+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 file
+    """
+    try:
+        with salt.utils.files.fopen(path, "rb") as rfh:
+            lines = salt.utils.stringutils.to_unicode(rfh.read()).splitlines()
+            try:
+                lines.remove("")
+            except ValueError:
+                pass
+            return lines
+    except Exception:  # pylint: disable=broad-except
+        return []  # Return empty list for type consistency
+
+
+def _write_file_iface(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.fopen(filename, 'w') as fp_:
+        fp_.write(salt.utils.stringutils.to_str(data))
+
+
+def _write_file_network(data, filename):
+    """
+    Writes a file to disk
+    """
+    with salt.utils.files.fopen(filename, "w") as fp_:
+        fp_.write(salt.utils.stringutils.to_str(data))
+
+
+def _read_temp(data):
+    lines = data.splitlines()
+    try:  # Discard newlines if they exist
+        lines.remove("")
+    except ValueError:
+        pass
+    return lines
+
+
+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)
+
+    if iface_type == "bond":
+        if "mode" not in settings:
+            msg = "mode is required for bond interfaces"
+            log.error(msg)
+            raise AttributeError(msg)
+        settings["mode"] = str(settings["mode"])
+
+    if iface_type == "vlan":
+        settings["vlan"] = "yes"
+
+    if iface_type == "bridge" and not __salt__["pkg.version"]("bridge-utils"):
+        __salt__["pkg.install"]("bridge-utils")
+
+    if iface_type in (
+        "eth",
+        "bond",
+        "bridge",
+        "slave",
+        "vlan",
+        "ipip",
+        "ib",
+        "alias",
+    ):
+        opts = _parse_settings_eth(settings, iface_type, enabled, iface)
+        try:
+            template = JINJA.get_template("ifcfg.jinja")
+        except jinja2.exceptions.TemplateNotFound:
+            log.error("Could not load template ifcfg.jinja")
+            return ""
+        log.debug("Interface opts: \n %s", opts)
+        ifcfg = template.render(opts)
+
+    if settings.get("test"):
+        return _read_temp(ifcfg)
+
+    _write_file_iface(iface, ifcfg, _SUSE_NETWORK_SCRIPT_DIR, "ifcfg-{0}")
+    path = os.path.join(_SUSE_NETWORK_SCRIPT_DIR, "ifcfg-{0}".format(iface))
+
+    return _read_file(path)
+
+
+def build_routes(iface, **settings):
+    """
+    Build a route script for a network interface.
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt '*' ip.build_routes eth0 <settings>
+    """
+
+    template = "ifroute.jinja"
+    log.debug("Template name: %s", template)
+
+    opts = _parse_routes(iface, settings)
+    log.debug("Opts: \n %s", opts)
+    try:
+        template = JINJA.get_template(template)
+    except jinja2.exceptions.TemplateNotFound:
+        log.error("Could not load template %s", template)
+        return ""
+    log.debug("IP routes:\n%s", opts["routes"])
+
+    if iface == "routes":
+        routecfg = template.render(routes=opts["routes"])
+    else:
+        routecfg = template.render(routes=opts["routes"], iface=iface)
+
+    if settings["test"]:
+        return _read_temp(routecfg)
+
+    if iface == "routes":
+        path = _SUSE_NETWORK_ROUTES_FILE
+    else:
+        path = os.path.join(_SUSE_NETWORK_SCRIPT_DIR, "ifroute-{0}".format(iface))
+
+    _write_file_network(routecfg, path)
+
+    return _read_file(path)
+
+
+def down(iface, iface_type=None):
+    """
+    Shutdown a network interface
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt '*' ip.down eth0
+    """
+    # Slave devices are controlled by the master.
+    if not iface_type or iface_type.lower() != "slave":
+        return __salt__["cmd.run"]("ifdown {0}".format(iface))
+    return None
+
+
+def get_interface(iface):
+    """
+    Return the contents of an interface script
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt '*' ip.get_interface eth0
+    """
+    path = os.path.join(_SUSE_NETWORK_SCRIPT_DIR, "ifcfg-{0}".format(iface))
+    return _read_file(path)
+
+
+def up(iface, iface_type=None):
+    """
+    Start up a network interface
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt '*' ip.up eth0
+    """
+    # Slave devices are controlled by the master.
+    if not iface_type or iface_type.lower() != "slave":
+        return __salt__["cmd.run"]("ifup {0}".format(iface))
+    return None
+
+
+def get_routes(iface):
+    """
+    Return the contents of the interface routes script.
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt '*' ip.get_routes eth0
+    """
+    if iface == "routes":
+        path = _SUSE_NETWORK_ROUTES_FILE
+    else:
+        path = os.path.join(_SUSE_NETWORK_SCRIPT_DIR, "ifroute-{0}".format(iface))
+    return _read_file(path)
+
+
+def get_network_settings():
+    """
+    Return the contents of the global network script.
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt '*' ip.get_network_settings
+    """
+    return _read_file(_SUSE_NETWORK_FILE)
+
+
+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:
+        res = __salt__["service.reload"]("network")
+
+    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>
+    """
+    # Read current configuration and store default values
+    current_network_settings = _parse_suse_config(_SUSE_NETWORK_FILE)
+
+    # Build settings
+    opts = _parse_network_settings(settings, current_network_settings)
+    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 settings["test"]:
+        return _read_temp(network)
+
+    # Write settings
+    _write_file_network(network, _SUSE_NETWORK_FILE)
+
+    __salt__["cmd.run"]("netconfig update -f")
+
+    return _read_file(_SUSE_NETWORK_FILE)
diff --git a/salt/states/network.py b/salt/states/network.py
index 3d8cc46a1c..7d691c3015 100644
--- a/salt/states/network.py
+++ b/salt/states/network.py
@@ -389,6 +389,8 @@ def managed(name, type, enabled=True, **kwargs):
     # set ranged status
     apply_ranged_setting = False
 
+    is_suse = (__grains__["os_family"] == "Suse")
+
     # Build interface
     try:
         old = __salt__['ip.get_interface'](name)
@@ -428,7 +430,7 @@ def managed(name, type, enabled=True, **kwargs):
         return ret
 
     # Setup up bond modprobe script if required
-    if type == 'bond':
+    if type == 'bond' and not is_suse:
         try:
             old = __salt__['ip.get_bond'](name)
             new = __salt__['ip.build_bond'](name, **kwargs)
@@ -519,22 +521,27 @@ def managed(name, type, enabled=True, **kwargs):
             # Check that there are new slaves for this master
             present_slaves = __salt__['cmd.run'](
                 ['cat', '/sys/class/net/{0}/bonding/slaves'.format(name)]).split()
-            desired_slaves = kwargs['slaves'].split()
+            if isinstance(kwargs['slaves'], list):
+                desired_slaves = kwargs['slaves']
+            else:
+                desired_slaves = kwargs['slaves'].split()
             missing_slaves = set(desired_slaves) - set(present_slaves)
 
             # Enslave only slaves missing in master
             if missing_slaves:
-                ifenslave_path = __salt__['cmd.run'](['which', 'ifenslave']).strip()
-                if ifenslave_path:
-                    log.info("Adding slaves '%s' to the master %s",
-                             ' '.join(missing_slaves), name)
-                    cmd = [ifenslave_path, name] + list(missing_slaves)
-                    __salt__['cmd.run'](cmd, python_shell=False)
-                else:
-                    log.error("Command 'ifenslave' not found")
-                ret['changes']['enslave'] = (
-                    "Added slaves '{0}' to master '{1}'"
-                    .format(' '.join(missing_slaves), name))
+                log.debug("Missing slaves of {0}: {1}".format(name, missing_slaves))
+                if not is_suse:
+                    ifenslave_path = __salt__['cmd.run'](['which', 'ifenslave']).strip()
+                    if ifenslave_path:
+                        log.info("Adding slaves '%s' to the master %s",
+                                 ' '.join(missing_slaves), name)
+                        cmd = [ifenslave_path, name] + list(missing_slaves)
+                        __salt__['cmd.run'](cmd, python_shell=False)
+                    else:
+                        log.error("Command 'ifenslave' not found")
+                    ret['changes']['enslave'] = (
+                        "Added slaves '{0}' to master '{1}'"
+                        .format(' '.join(missing_slaves), name))
             else:
                 log.info("All slaves '%s' are already added to the master %s"
                          ", no actions required",
diff --git a/salt/templates/suse_ip/ifcfg.jinja b/salt/templates/suse_ip/ifcfg.jinja
new file mode 100644
index 0000000000..8384d0eab7
--- /dev/null
+++ b/salt/templates/suse_ip/ifcfg.jinja
@@ -0,0 +1,34 @@
+{% if nickname %}NAME='{{nickname}}'
+{%endif%}{% if startmode %}STARTMODE='{{startmode}}'
+{%endif%}{% if proto %}BOOTPROTO='{{proto}}'
+{%endif%}{% if uuid %}UUID='{{uuid}}'
+{%endif%}{% if vlan %}VLAN='{{vlan}}'
+{%endif%}{% if team_config %}TEAM_CONFIG='{{team_config}}'
+{%endif%}{% if team_port_config %}TEAM_PORT_CONFIG='{{team_port_config}}'
+{%endif%}{% if team_master %}TEAM_MASTER='{{team_master}}'
+{%endif%}{% if ipaddr %}IPADDR='{{ipaddr}}'
+{%endif%}{% if netmask %}NETMASK='{{netmask}}'
+{%endif%}{% if prefix %}PREFIXLEN="{{prefix}}"
+{%endif%}{% if ipaddrs %}{% for i in ipaddrs -%}
+IPADDR{{loop.index}}='{{i}}'
+{% endfor -%}
+{%endif%}{% if clonenum_start %}CLONENUM_START="{{clonenum_start}}"
+{%endif%}{% if gateway %}GATEWAY="{{gateway}}"
+{%endif%}{% if arpcheck %}ARPCHECK="{{arpcheck}}"
+{%endif%}{% if srcaddr %}SRCADDR="{{srcaddr}}"
+{%endif%}{% if defroute %}DEFROUTE="{{defroute}}"
+{%endif%}{% if bridge %}BRIDGE="{{bridge}}"
+{%endif%}{% if stp %}STP="{{stp}}"
+{%endif%}{% if delay or delay == 0 %}DELAY="{{delay}}"
+{%endif%}{% if mtu %}MTU='{{mtu}}'
+{%endif%}{% if zone %}ZONE='{{zone}}'
+{%endif%}{% if bonding %}BONDING_MODULE_OPTS='{{bonding}}'
+BONDING_MASTER='yes'
+{% for sl in slaves -%}
+BONDING_SLAVE{{loop.index}}='{{sl}}'
+{% endfor -%}
+{%endif%}{% if ethtool %}ETHTOOL_OPTIONS='{{ethtool}}'
+{%endif%}{% if phys_dev %}ETHERDEVICE='{{phys_dev}}'
+{%endif%}{% if vlan_id %}VLAN_ID='{{vlan_id}}'
+{%endif%}{% if userctl %}USERCONTROL='{{userctl}}'
+{%endif%}
diff --git a/salt/templates/suse_ip/ifroute.jinja b/salt/templates/suse_ip/ifroute.jinja
new file mode 100644
index 0000000000..0081e4c688
--- /dev/null
+++ b/salt/templates/suse_ip/ifroute.jinja
@@ -0,0 +1,8 @@
+{%- for route in routes -%}
+{% if route.name %}# {{route.name}} {%- endif %}
+{{ route.ipaddr }}
+{%- if route.gateway %}	{{route.gateway}}{% else %}	-{% endif %}
+{%- if route.netmask %}	{{route.netmask}}{% else %}	-{% endif %}
+{%- if route.dev %}	{{route.dev}}{% else %}{%- if iface and iface != "routes" %}	{{iface}}{% else %}	-{% endif %}{% endif %}
+{%- if route.metric %}	metric {{route.metric}} {%- endif %}
+{% endfor -%}
diff --git a/salt/templates/suse_ip/network.jinja b/salt/templates/suse_ip/network.jinja
new file mode 100644
index 0000000000..64ae911271
--- /dev/null
+++ b/salt/templates/suse_ip/network.jinja
@@ -0,0 +1,30 @@
+{% if auto6_wait_at_boot %}AUTO6_WAIT_AT_BOOT="{{auto6_wait_at_boot}}"
+{%endif%}{% if auto6_update %}AUTO6_UPDATE="{{auto6_update}}"
+{%endif%}{% if link_required %}LINK_REQUIRED="{{link_required}}"
+{%endif%}{% if wicked_debug %}WICKED_DEBUG="{{wicked_debug}}"
+{%endif%}{% if wicked_log_level %}WICKED_LOG_LEVEL="{{wicked_log_level}}"
+{%endif%}{% if check_duplicate_ip %}CHECK_DUPLICATE_IP="{{check_duplicate_ip}}"
+{%endif%}{% if send_gratuitous_arp %}SEND_GRATUITOUS_ARP="{{send_gratuitous_arp}}"
+{%endif%}{% if debug %}DEBUG="{{debug}}"
+{%endif%}{% if wait_for_interfaces %}WAIT_FOR_INTERFACES="{{wait_for_interfaces}}"
+{%endif%}{% if firewall %}FIREWALL="{{firewall}}"
+{%endif%}{% if nm_online_timeout %}NM_ONLINE_TIMEOUT="{{nm_online_timeout}}"
+{%endif%}{% if netconfig_modules_order %}NETCONFIG_MODULES_ORDER="{{netconfig_modules_order}}"
+{%endif%}{% if netconfig_verbose %}NETCONFIG_VERBOSE="{{netconfig_verbose}}"
+{%endif%}{% if netconfig_force_replace %}NETCONFIG_FORCE_REPLACE="{{netconfig_force_replace}}"
+{%endif%}{% if dns_policy %}NETCONFIG_DNS_POLICY="{{dns_policy}}"
+{%endif%}{% if dns_forwarder %}NETCONFIG_DNS_FORWARDER="{{dns_forwarder}}"
+{%endif%}{% if dns_forwarder_fallback %}NETCONFIG_DNS_FORWARDER_FALLBACK="{{dns_forwarder_fallback}}"
+{%endif%}{% if dns_search %}NETCONFIG_DNS_STATIC_SEARCHLIST="{{ dns_search|join(' ') }}"
+{%endif%}{% if dns %}NETCONFIG_DNS_STATIC_SERVERS="{{ dns|join(' ') }}"
+{%endif%}{% if dns_ranking %}NETCONFIG_DNS_RANKING="{{dns_ranking}}"
+{%endif%}{% if dns_resolver_options %}NETCONFIG_DNS_RESOLVER_OPTIONS="{{dns_resolver_options}}"
+{%endif%}{% if dns_resolver_sortlist %}NETCONFIG_DNS_RESOLVER_SORTLIST="{{dns_resolver_sortlist}}"
+{%endif%}{% if ntp_policy %}NETCONFIG_NTP_POLICY="{{ntp_policy}}"
+{%endif%}{% if ntp_static_servers %}NETCONFIG_NTP_STATIC_SERVERS="{{ntp_static_servers}}"
+{%endif%}{% if nis_policy %}NETCONFIG_NIS_POLICY="{{nis_policy}}"
+{%endif%}{% if nis_setdomainname %}NETCONFIG_NIS_SETDOMAINNAME="{{nis_setdomainname}}"
+{%endif%}{% if nis_static_domain %}NETCONFIG_NIS_STATIC_DOMAIN="{{nis_static_domain}}"
+{%endif%}{% if nis_static_servers %}NETCONFIG_NIS_STATIC_SERVERS="{{nis_static_servers}}"
+{%endif%}{% if wireless_regulatory_domain %}WIRELESS_REGULATORY_DOMAIN="{{wireless_regulatory_domain}}"
+{%endif%}
diff --git a/setup.py b/setup.py
index 67a915c64c..808e0a6923 100755
--- a/setup.py
+++ b/setup.py
@@ -957,6 +957,7 @@ class SaltDistribution(distutils.dist.Distribution):
     @property
     def _property_package_data(self):
         package_data = {'salt.templates': ['rh_ip/*.jinja',
+                                           'suse_ip/*.jinja',
                                            'debian_ip/*.jinja',
                                            'virt/*.jinja',
                                            'git/*',
-- 
2.30.0


