fixes saltstack/salt#63128 add ethtool execution and state module functions for pause

This commit is contained in:
nicholasmhughes 2022-11-28 16:57:54 -05:00 committed by Megan Wilhite
parent b7da9d2ef2
commit 553dfd2fe2
5 changed files with 545 additions and 1 deletions

1
changelog/63128.added Normal file
View file

@ -0,0 +1 @@
Add ethtool execution and state module functions for pause

View file

@ -9,8 +9,11 @@ Module for running ethtool command
:platform: linux
"""
import logging
import os
import salt.utils.path
from salt.exceptions import CommandExecutionError
try:
import ethtool
@ -299,3 +302,136 @@ def set_offload(devname, **kwargs):
return "Not supported"
return show_offload(devname)
def _ethtool_command(devname, *args, **kwargs):
"""
Helper function to build an ethtool command
"""
ethtool = salt.utils.path.which("ethtool")
if not ethtool:
raise CommandExecutionError("Command 'ethtool' cannot be found")
switches = " ".join(arg for arg in args)
params = " ".join("{} {}".format(key, val) for key, val in kwargs.items())
cmd = "{} {} {} {}".format(ethtool, switches, devname, params).strip()
ret = __salt__["cmd.run"](cmd, ignore_retcode=True).splitlines()
if ret and ret[0].startswith("Cannot"):
raise CommandExecutionError(ret[0])
return ret
def _validate_params(valid_params, kwargs):
"""
Helper function to validate parameters to ethtool commands. Boolean values
will be transformed into ``on`` and ``off`` to match expected syntax.
"""
validated = {}
for key, val in kwargs.items():
key = key.lower()
if key in valid_params:
if val is True:
val = "on"
elif val is False:
val = "off"
validated[key] = val
if not validated:
raise CommandExecutionError(
"None of the valid parameters were provided: {}".format(valid_params)
)
return validated
def show_pause(devname):
"""
Queries the specified network device for associated pause information
CLI Example:
.. code-block:: bash
salt '*' ethtool.show_pause <devname>
"""
data = {}
content = _ethtool_command(devname, "-a")
for line in content[1:]:
if line.strip():
(key, value) = (s.strip() for s in line.split(":", 1))
data[key] = value == "on"
return data
def set_pause(devname, **kwargs):
"""
Changes the pause parameters of the specified network device
CLI Example:
.. code-block:: bash
salt '*' ethtool.set_pause <devname> autoneg=off rx=off tx=off
"""
valid_params = ["autoneg", "rx", "tx"]
params = _validate_params(valid_params, kwargs)
ret = _ethtool_command(devname, "-A", **params)
if not ret:
return True
return ret
def show_features(devname):
"""
Queries the specified network device for associated feature information
CLI Example:
.. code-block:: bash
salt '*' ethtool.show_feature <devname>
"""
data = {}
content = _ethtool_command(devname, "-k")
for line in content[1:]:
if ":" in line:
key, value = (s.strip() for s in line.strip().split(":", 1))
fixed = "fixed" in value
if fixed:
value = value.split()[0].strip()
data[key.strip()] = {"on": value == "on", "fixed": fixed}
return data
def set_feature(devname, **kwargs):
"""
Changes the feature parameters of the specified network device
CLI Example:
.. code-block:: bash
salt '*' ethtool.set_feature <devname> sg=off
"""
valid_params = [
"rx",
"tx",
"sg",
"tso",
"ufo",
"gso",
"gro",
"lro",
"rxvlan",
"txvlan",
"ntuple",
"rxhash",
]
params = _validate_params(valid_params, kwargs)
ret = _ethtool_command(devname, "-K", **params)
if not ret:
return True
return os.linesep.join(ret)

View file

@ -32,6 +32,8 @@ Configuration of network device
import logging
from salt.exceptions import CommandExecutionError
# Set up logging
log = logging.getLogger(__name__)
@ -310,3 +312,83 @@ def offload(name, **kwargs):
return ret
return ret
def pause(name, **kwargs):
"""
Manage pause parameters of network device
name
Interface name to apply pause parameters
.. code-block:: yaml
eth0:
ethtool.pause:
- name: eth0
- autoneg: off
- rx: off
- tx: off
"""
ret = {
"name": name,
"changes": {},
"result": True,
"comment": "Network device {} pause parameters are up to date.".format(name),
}
apply_pause = False
# Get current pause parameters
try:
old = __salt__["ethtool.show_pause"](name)
except CommandExecutionError:
ret["result"] = False
ret["comment"] = "Device {} pause parameters are not supported".format(name)
return ret
# map ethtool command input to output text
pause_map = {
"autoneg": "Autonegotiate",
"rx": "RX",
"tx": "RX",
}
# Process changes
new = {}
diff = []
for key, value in kwargs.items():
key = key.lower()
if key in pause_map:
if value != old[pause_map[key]]:
new.update({key: value})
if value is True:
value = "on"
elif value is False:
value = "off"
diff.append("{}: {}".format(key, value))
if not new:
return ret
# Dry run
if __opts__["test"]:
ret["result"] = None
ret["comment"] = "Device {} pause parameters are set to be updated:\n{}".format(
name, "\n".join(diff)
)
return ret
# Apply pause parameters
try:
__salt__["ethtool.set_pause"](name, **new)
# Prepare return output
ret["comment"] = "Device {} pause parameters updated.".format(name)
ret["changes"]["ethtool_pause"] = "\n".join(diff)
except CommandExecutionError as exc:
ret["result"] = False
ret["comment"] = str(exc)
return ret
return ret

View file

@ -0,0 +1,248 @@
from textwrap import dedent
import pytest
import salt.modules.ethtool as ethtool
from salt.exceptions import CommandExecutionError
from tests.support.mock import MagicMock, patch
@pytest.fixture
def configure_loader_modules():
return {
ethtool: {
"__salt__": {},
}
}
@pytest.fixture(scope="module")
def pause_ret():
cmdret = dedent(
"""Pause parameters for eth0:
Autonegotiate: on
RX: on
TX: on
RX negotiated: off
TX negotiated: off"""
)
return cmdret
@pytest.fixture(scope="module")
def features_ret():
cmdret = dedent(
"""Features for eth0:
rx-checksumming: on [fixed]
tx-checksumming: on
tx-checksum-ipv4: off [fixed]
tx-checksum-ip-generic: on
tx-checksum-ipv6: off [fixed]
tx-checksum-fcoe-crc: off [fixed]
tx-checksum-sctp: off [fixed]
scatter-gather: on
tx-scatter-gather: on
tx-scatter-gather-fraglist: off [fixed]
tcp-segmentation-offload: on
tx-tcp-segmentation: on
tx-tcp-ecn-segmentation: on
tx-tcp-mangleid-segmentation: off
tx-tcp6-segmentation: on
udp-fragmentation-offload: off
generic-segmentation-offload: on
generic-receive-offload: on
large-receive-offload: off [fixed]
rx-vlan-offload: off [fixed]
tx-vlan-offload: off [fixed]
ntuple-filters: off [fixed]
receive-hashing: off [fixed]
highdma: on [fixed]
rx-vlan-filter: on [fixed]
vlan-challenged: off [fixed]
tx-lockless: off [fixed]
netns-local: off [fixed]
tx-gso-robust: on [fixed]
tx-fcoe-segmentation: off [fixed]
tx-gre-segmentation: off [fixed]
tx-gre-csum-segmentation: off [fixed]
tx-ipxip4-segmentation: off [fixed]
tx-ipxip6-segmentation: off [fixed]
tx-udp_tnl-segmentation: off [fixed]
tx-udp_tnl-csum-segmentation: off [fixed]
tx-gso-partial: off [fixed]
tx-sctp-segmentation: off [fixed]
tx-esp-segmentation: off [fixed]
tx-udp-segmentation: off [fixed]
fcoe-mtu: off [fixed]
tx-nocache-copy: off
loopback: off [fixed]
rx-fcs: off [fixed]
rx-all: off [fixed]
tx-vlan-stag-hw-insert: off [fixed]
rx-vlan-stag-hw-parse: off [fixed]
rx-vlan-stag-filter: off [fixed]
l2-fwd-offload: off [fixed]
hw-tc-offload: off [fixed]
esp-hw-offload: off [fixed]
esp-tx-csum-hw-offload: off [fixed]
rx-udp_tunnel-port-offload: off [fixed]
tls-hw-tx-offload: off [fixed]
tls-hw-rx-offload: off [fixed]
rx-gro-hw: off [fixed]
tls-hw-record: off [fixed]"""
)
return cmdret
def test_ethtool__ethtool_command_which_fail():
with patch("salt.utils.path.which", MagicMock(return_value=None)):
with pytest.raises(CommandExecutionError):
ethtool._ethtool_command("eth0")
def test_ethtool__ethtool_command_operation_not_supported():
mock_cmd_run = MagicMock(
side_effect=[
"Pause parameters for eth0:\nCannot get device pause settings: Operation not supported",
"Cannot get device pause settings: Operation not supported",
]
)
with patch(
"salt.utils.path.which", MagicMock(return_value="/sbin/ethtool")
), patch.dict(ethtool.__salt__, {"cmd.run": mock_cmd_run}):
with pytest.raises(CommandExecutionError):
ethtool._ethtool_command("eth0", "-a")
ethtool._ethtool_command("eth0", "-A", autoneg="off", rx="off", tx="off")
def test_ethtool__ethtool_command(pause_ret):
mock_cmd_run = MagicMock(return_value=pause_ret)
with patch(
"salt.utils.path.which", MagicMock(return_value="/sbin/ethtool")
), patch.dict(ethtool.__salt__, {"cmd.run": mock_cmd_run}):
ret = ethtool._ethtool_command("eth0", "-A", autoneg="off", rx="off", tx="off")
mock_cmd_run.assert_called_once_with(
"/sbin/ethtool -A eth0 autoneg off rx off tx off", ignore_retcode=True
)
assert pause_ret.splitlines() == ret
def test_ethtool__validate_params():
with pytest.raises(CommandExecutionError):
ethtool._validate_params(["not_found"], {"eth": "tool"})
assert ethtool._validate_params(["eth"], {"eth": "tool"}) == {"eth": "tool"}
assert ethtool._validate_params(["eth", "not_found"], {"eth": "tool"}) == {
"eth": "tool"
}
assert ethtool._validate_params(["eth", "salt"], {"eth": True, "salt": False}) == {
"eth": "on",
"salt": "off",
}
def test_ethtool_show_pause(pause_ret):
expected = {
"Autonegotiate": True,
"RX": True,
"RX negotiated": False,
"TX": True,
"TX negotiated": False,
}
with patch(
"salt.modules.ethtool._ethtool_command",
MagicMock(return_value=pause_ret.splitlines()),
):
ret = ethtool.show_pause("eth0")
assert expected == ret
def test_ethtool_show_features(features_ret):
expected = {
"esp-hw-offload": {"fixed": True, "on": False},
"esp-tx-csum-hw-offload": {"fixed": True, "on": False},
"fcoe-mtu": {"fixed": True, "on": False},
"generic-receive-offload": {"fixed": False, "on": True},
"generic-segmentation-offload": {"fixed": False, "on": True},
"highdma": {"fixed": True, "on": True},
"hw-tc-offload": {"fixed": True, "on": False},
"l2-fwd-offload": {"fixed": True, "on": False},
"large-receive-offload": {"fixed": True, "on": False},
"loopback": {"fixed": True, "on": False},
"netns-local": {"fixed": True, "on": False},
"ntuple-filters": {"fixed": True, "on": False},
"receive-hashing": {"fixed": True, "on": False},
"rx-all": {"fixed": True, "on": False},
"rx-checksumming": {"fixed": True, "on": True},
"rx-fcs": {"fixed": True, "on": False},
"rx-gro-hw": {"fixed": True, "on": False},
"rx-udp_tunnel-port-offload": {"fixed": True, "on": False},
"rx-vlan-filter": {"fixed": True, "on": True},
"rx-vlan-offload": {"fixed": True, "on": False},
"rx-vlan-stag-filter": {"fixed": True, "on": False},
"rx-vlan-stag-hw-parse": {"fixed": True, "on": False},
"scatter-gather": {"fixed": False, "on": True},
"tcp-segmentation-offload": {"fixed": False, "on": True},
"tls-hw-record": {"fixed": True, "on": False},
"tls-hw-rx-offload": {"fixed": True, "on": False},
"tls-hw-tx-offload": {"fixed": True, "on": False},
"tx-checksum-fcoe-crc": {"fixed": True, "on": False},
"tx-checksum-ip-generic": {"fixed": False, "on": True},
"tx-checksum-ipv4": {"fixed": True, "on": False},
"tx-checksum-ipv6": {"fixed": True, "on": False},
"tx-checksum-sctp": {"fixed": True, "on": False},
"tx-checksumming": {"fixed": False, "on": True},
"tx-esp-segmentation": {"fixed": True, "on": False},
"tx-fcoe-segmentation": {"fixed": True, "on": False},
"tx-gre-csum-segmentation": {"fixed": True, "on": False},
"tx-gre-segmentation": {"fixed": True, "on": False},
"tx-gso-partial": {"fixed": True, "on": False},
"tx-gso-robust": {"fixed": True, "on": True},
"tx-ipxip4-segmentation": {"fixed": True, "on": False},
"tx-ipxip6-segmentation": {"fixed": True, "on": False},
"tx-lockless": {"fixed": True, "on": False},
"tx-nocache-copy": {"fixed": False, "on": False},
"tx-scatter-gather": {"fixed": False, "on": True},
"tx-scatter-gather-fraglist": {"fixed": True, "on": False},
"tx-sctp-segmentation": {"fixed": True, "on": False},
"tx-tcp-ecn-segmentation": {"fixed": False, "on": True},
"tx-tcp-mangleid-segmentation": {"fixed": False, "on": False},
"tx-tcp-segmentation": {"fixed": False, "on": True},
"tx-tcp6-segmentation": {"fixed": False, "on": True},
"tx-udp-segmentation": {"fixed": True, "on": False},
"tx-udp_tnl-csum-segmentation": {"fixed": True, "on": False},
"tx-udp_tnl-segmentation": {"fixed": True, "on": False},
"tx-vlan-offload": {"fixed": True, "on": False},
"tx-vlan-stag-hw-insert": {"fixed": True, "on": False},
"udp-fragmentation-offload": {"fixed": False, "on": False},
"vlan-challenged": {"fixed": True, "on": False},
}
with patch(
"salt.modules.ethtool._ethtool_command",
MagicMock(return_value=features_ret.splitlines()),
):
ret = ethtool.show_features("eth0")
assert expected == ret
def test_ethtool_set_pause():
with patch("salt.modules.ethtool._ethtool_command", MagicMock(return_value="")):
with pytest.raises(CommandExecutionError):
ethtool.set_pause("eth0", not_there=False)
ret = ethtool.set_pause("eth0", autoneg=False)
assert ret is True
def test_ethtool_set_feature():
with patch("salt.modules.ethtool._ethtool_command", MagicMock(return_value="")):
with pytest.raises(CommandExecutionError):
ethtool.set_feature("eth0", not_there=False)
ret = ethtool.set_feature("eth0", sg=False)
assert ret is True

View file

@ -0,0 +1,77 @@
import pytest
import salt.states.ethtool as ethtool
from salt.exceptions import CommandExecutionError
from tests.support.mock import MagicMock, patch
@pytest.fixture
def configure_loader_modules():
return {
ethtool: {
"__opts__": {"test": False},
"__salt__": {},
}
}
def test_ethtool_pause():
expected = {
"changes": {},
"comment": "Network device eth0 pause parameters are up to date.",
"name": "eth0",
"result": True,
}
show_ret = {
"Autonegotiate": True,
"RX": True,
"RX negotiated": False,
"TX": True,
"TX negotiated": False,
}
mock_show = MagicMock(return_value=show_ret)
mock_set = MagicMock(return_value=True)
with patch.dict(
ethtool.__salt__,
{"ethtool.set_pause": mock_set, "ethtool.show_pause": mock_show},
):
# clean
ret = ethtool.pause("eth0", autoneg=True, rx=True, tx=True)
assert ret == expected
# changes
expected["changes"] = {"ethtool_pause": "autoneg: off\nrx: off\ntx: off"}
expected["comment"] = "Device eth0 pause parameters updated."
ret = ethtool.pause("eth0", autoneg=False, rx=False, tx=False)
assert ret == expected
mock_set.assert_called_once_with("eth0", autoneg=False, rx=False, tx=False)
# changes, test mode
mock_set.reset_mock()
with patch.dict(ethtool.__opts__, {"test": True}):
expected["result"] = None
expected["changes"] = {}
expected[
"comment"
] = "Device eth0 pause parameters are set to be updated:\nautoneg: off\nrx: off\ntx: off"
ret = ethtool.pause("eth0", autoneg=False, rx=False, tx=False)
assert ret == expected
mock_set.assert_not_called()
# exceptions
with patch.dict(
ethtool.__salt__,
{
"ethtool.set_pause": MagicMock(side_effect=CommandExecutionError("blargh")),
"ethtool.show_pause": MagicMock(
side_effect=[CommandExecutionError, show_ret]
),
},
):
expected["comment"] = "Device eth0 pause parameters are not supported"
expected["result"] = False
ret = ethtool.pause("eth0", autoneg=False, rx=False, tx=False)
assert ret == expected
ret = ethtool.pause("eth0", autoneg=False, rx=False, tx=False)
expected["comment"] = "blargh"
assert ret == expected