Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
openSUSE:Leap:15.4:ARM
salt.24049
implementation-of-suse_ip-execution-module-bsc-...
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File implementation-of-suse_ip-execution-module-bsc-10999.patch of Package salt.24049
From ebf90aaad969a61708673a9681d0d534134e16f8 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 | 28 +- 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(+), 8 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 d3bab3a1f8..790241a82e 100644 --- a/salt/modules/rh_ip.py +++ b/salt/modules/rh_ip.py @@ -551,7 +551,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 f20863113b..49d7857f1d 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,17 +651,29 @@ def managed(name, enabled=True, **kwargs): present_slaves = __salt__["cmd.run"]( ["cat", "/sys/class/net/{}/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) 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 e13e5485ed..866e8d91f9 100755 --- a/setup.py +++ b/setup.py @@ -1106,6 +1106,7 @@ class SaltDistribution(distutils.dist.Distribution): package_data = { "salt.templates": [ "rh_ip/*.jinja", + "suse_ip/*.jinja", "debian_ip/*.jinja", "virt/*.jinja", "git/*", -- 2.33.0
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor