File implementation-of-suse_ip-execution-module-bsc-10999.patch of Package salt.21868
From fc15e6791deaac9b5ac52268b218e202481440a4 Mon Sep 17 00:00:00 2001
From: Victor Zhestkov <35733135+vzhestkov@users.noreply.github.com>
Date: Thu, 18 Feb 2021 15:56:01 +0300
Subject: [PATCH] Implementation of suse_ip execution module
(bsc#1099976) (#323)
---
salt/modules/linux_ip.py | 2 +
salt/modules/rh_ip.py | 2 +-
salt/modules/suse_ip.py | 1151 ++++++++++++++++++++++++++
salt/states/network.py | 35 +-
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, 1248 insertions(+), 15 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 bac0665de2..e7a268694d 100644
--- a/salt/modules/linux_ip.py
+++ b/salt/modules/linux_ip.py
@@ -21,6 +21,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 2da954bdd0..fa13cc85d1 100644
--- a/salt/modules/rh_ip.py
+++ b/salt/modules/rh_ip.py
@@ -543,7 +543,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 30bd07810e..04d6a71f34 100644
--- a/salt/states/network.py
+++ b/salt/states/network.py
@@ -504,6 +504,8 @@ def managed(name, enabled=True, **kwargs):
msg += " Update your SLS file to get rid of this warning."
ret.setdefault("warnings", []).append(msg)
+ is_suse = (__grains__["os_family"] == "Suse")
+
# Build interface
try:
old = __salt__["ip.get_interface"](name)
@@ -649,25 +651,30 @@ def managed(name, enabled=True, **kwargs):
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,
+ 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
)
- 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"
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 d9c3d6e303..c6bd4a3c03 100755
--- a/setup.py
+++ b/setup.py
@@ -1108,6 +1108,7 @@ class SaltDistribution(distutils.dist.Distribution):
package_data = {
"salt.templates": [
"rh_ip/*.jinja",
+ "suse_ip/*.jinja",
"debian_ip/*.jinja",
"virt/*.jinja",
"git/*",
--
2.30.0