mirror of
https://github.com/saltstack/salt.git
synced 2025-04-16 09:40:20 +00:00
fixes saltstack/salt#63128 add ethtool execution and state module functions for pause
This commit is contained in:
parent
b7da9d2ef2
commit
553dfd2fe2
5 changed files with 545 additions and 1 deletions
1
changelog/63128.added
Normal file
1
changelog/63128.added
Normal file
|
@ -0,0 +1 @@
|
|||
Add ethtool execution and state module functions for pause
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
248
tests/pytests/unit/modules/test_ethtool.py
Normal file
248
tests/pytests/unit/modules/test_ethtool.py
Normal 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
|
77
tests/pytests/unit/states/test_ethtool.py
Normal file
77
tests/pytests/unit/states/test_ethtool.py
Normal 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
|
Loading…
Add table
Reference in a new issue