Skip to content

Commit

Permalink
Add tests for issues saltstack#64531 and saltstack#52452
Browse files Browse the repository at this point in the history
  • Loading branch information
lkubb committed Dec 18, 2023
1 parent 21391fe commit e89daea
Show file tree
Hide file tree
Showing 4 changed files with 368 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""
Verify salt-ssh stops state execution and fails with a retcode > 0
when a state rendering fails because an execution module throws an exception.
"""

import pytest

from salt.defaults.exitcodes import EX_AGGREGATE

pytestmark = [
pytest.mark.skip_on_windows(reason="salt-ssh not available on Windows"),
pytest.mark.slow_test,
]


@pytest.fixture(scope="module", autouse=True)
def state_tree_render_module_exception(base_env_state_tree_root_dir):
top_file = """
base:
'localhost':
- fail_module_exception
'127.0.0.1':
- fail_module_exception
"""
state_file = r"""
This should fail being rendered:
test.show_notification:
- text: {{ salt["disk.usage"]("c") | yaml_dquote }}
"""
top_tempfile = pytest.helpers.temp_file(
"top.sls", top_file, base_env_state_tree_root_dir
)
state_tempfile = pytest.helpers.temp_file(
"fail_module_exception.sls", state_file, base_env_state_tree_root_dir
)
with top_tempfile, state_tempfile:
yield


@pytest.mark.parametrize(
"args,retcode",
(
(("state.sls", "fail_module_exception"), EX_AGGREGATE),
(("state.highstate",), EX_AGGREGATE),
(("state.sls_id", "foo", "fail_module_exception"), EX_AGGREGATE),
(("state.show_sls", "fail_module_exception"), EX_AGGREGATE),
(("state.show_low_sls", "fail_module_exception"), EX_AGGREGATE),
(("state.show_highstate",), EX_AGGREGATE),
# state.show_lowstate exits with 0 for non-ssh as well
(("state.show_lowstate",), 0),
(("state.top", "top.sls"), EX_AGGREGATE),
),
)
def test_it(salt_ssh_cli, args, retcode):
ret = salt_ssh_cli.run(*args)

assert ret.returncode == retcode
assert isinstance(ret.data, list)
assert ret.data
assert isinstance(ret.data[0], str)
# While these three should usually follow each other, there
# can be warnings in between that would break such a logic.
assert "Rendering SLS 'base:fail_module_exception' failed:" in ret.data[0]
assert "Problem running salt function in Jinja template:" in ret.data[0]
assert (
"Error running 'disk.usage': Invalid flag passed to disk.usage" in ret.data[0]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"""
Verify salt-ssh does not report success when it cannot parse
the return value.
"""

import pytest

from salt.defaults.exitcodes import EX_AGGREGATE

pytestmark = [
pytest.mark.skip_on_windows(reason="salt-ssh not available on Windows"),
pytest.mark.slow_test,
]


@pytest.fixture(scope="module")
def state_tree_remote_exception_mod(salt_run_cli, base_env_state_tree_root_dir):
module_contents = r"""
import os
import sys
def __virtual__():
return "whoops"
def do_stuff(name):
print("Hi there, nice that you're trying to make me do stuff.", file=sys.stderr)
print("Traceback (most recent call last):", file=sys.stderr)
print(' File "/dont/treat/me/like/that.py" in buzz_off', file=sys.stderr)
print("ComplianceError: 'Outlaw detected'", file=sys.stderr)
sys.stderr.flush()
os._exit(1)
"""
module_dir = base_env_state_tree_root_dir / "_states"
module_tempfile = pytest.helpers.temp_file("whoops.py", module_contents, module_dir)
try:
with module_tempfile:
ret = salt_run_cli.run("saltutil.sync_states")
assert ret.returncode == 0
assert "states.whoops" in ret.data
yield
finally:
ret = salt_run_cli.run("saltutil.sync_states")
assert ret.returncode == 0


@pytest.fixture(scope="module", autouse=True)
def state_tree_remote_exception(
base_env_state_tree_root_dir, state_tree_remote_exception_mod
):
top_file = """
base:
'localhost':
- remote_stacktrace
'127.0.0.1':
- remote_stacktrace
"""
state_file = r"""
This should be detected as failure:
whoops.do_stuff
"""
top_tempfile = pytest.helpers.temp_file(
"top.sls", top_file, base_env_state_tree_root_dir
)
state_tempfile = pytest.helpers.temp_file(
"remote_stacktrace.sls", state_file, base_env_state_tree_root_dir
)
with top_tempfile, state_tempfile:
yield


@pytest.mark.slow_test
@pytest.mark.usefixtures("state_tree_remote_exception")
@pytest.mark.parametrize(
"args",
(
("state.sls", "remote_stacktrace"),
("state.highstate",),
("state.sls_id", "This should be detected as failure", "remote_stacktrace"),
("state.top", "top.sls"),
("state.single", "whoops.do_stuff", "now"),
),
)
def test_it(salt_ssh_cli, args):
ret = salt_ssh_cli.run(*args)

assert ret.returncode == EX_AGGREGATE
assert ret.data
assert isinstance(ret.data, str)
assert "ComplianceError: 'Outlaw detected'" in ret.data
175 changes: 175 additions & 0 deletions tests/pytests/integration/ssh/test_deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,119 @@ def thin_dir(salt_ssh_cli):
shutil.rmtree(thin_dir_path, ignore_errors=True)


@pytest.fixture(scope="module")
def invalid_json_exe_mod(salt_run_cli, base_env_state_tree_root_dir):
module_contents = r"""
import os
import sys
def __virtual__():
return "whoops"
def test():
data = '{\n "local": {\n "whoops": "hrhrhr"\n }\n}'
ctr = 0
for line in data.splitlines():
sys.stdout.write(line)
if ctr == 3:
print("Warning: Chaos is not a letter")
ctr += 1
sys.stdout.flush()
os._exit(0)
"""
module_dir = base_env_state_tree_root_dir / "_modules"
module_tempfile = pytest.helpers.temp_file("whoops.py", module_contents, module_dir)
try:
with module_tempfile:
ret = salt_run_cli.run("saltutil.sync_modules")
assert ret.returncode == 0
assert "modules.whoops" in ret.data
yield
finally:
ret = salt_run_cli.run("saltutil.sync_modules")
assert ret.returncode == 0


@pytest.fixture(scope="module")
def invalid_return_exe_mod(salt_run_cli, base_env_state_tree_root_dir):
module_contents = r"""
import json
import os
import sys
def __virtual__():
return "whoopsiedoodle"
def test(wrapped=True):
data = "Chaos is a ladder though"
if wrapped:
data = {"local": {"no_return_key_present": data}}
else:
data = {"no_local_key_present": data}
print(json.dumps(data))
sys.stdout.flush()
os._exit(0)
"""
module_dir = base_env_state_tree_root_dir / "_modules"
module_tempfile = pytest.helpers.temp_file(
"whoopsiedoodle.py", module_contents, module_dir
)
try:
with module_tempfile:
ret = salt_run_cli.run("saltutil.sync_modules")
assert ret.returncode == 0
assert "modules.whoopsiedoodle" in ret.data
yield
finally:
ret = salt_run_cli.run("saltutil.sync_modules")
assert ret.returncode == 0


@pytest.fixture(scope="module")
def remote_exception_wrap_mod(salt_master):
module_contents = r"""
def __virtual__():
return "check_exception"
def failure():
# This should raise an exception
ret = __salt__["disk.usage"]("c")
return f"Probably got garbage: {ret}"
"""
module_dir = pathlib.Path(salt_master.config["extension_modules"]) / "wrapper"
module_tempfile = pytest.helpers.temp_file(
"check_exception.py", module_contents, module_dir
)
with module_tempfile:
yield


@pytest.fixture(scope="module")
def remote_parsing_failure_wrap_mod(salt_master, invalid_json_exe_mod):
module_contents = r"""
def __virtual__():
return "check_parsing"
def failure(mod):
# This should raise an exception
ret = __salt__[f"{mod}.test"]()
return f"Probably got garbage: {ret}"
"""
module_dir = pathlib.Path(salt_master.config["extension_modules"]) / "wrapper"
module_tempfile = pytest.helpers.temp_file(
"check_parsing.py", module_contents, module_dir
)
with module_tempfile:
yield


def test_ping(salt_ssh_cli):
"""
Test a simple ping
Expand Down Expand Up @@ -122,3 +235,65 @@ def test_retcode_exe_run_exception(salt_ssh_cli):
assert isinstance(ret.data, dict)
assert ret.data["stderr"].endswith("Exception: hehehe")
assert ret.data["retcode"] == 1


@pytest.mark.usefixtures("invalid_json_exe_mod")
def test_retcode_json_decode_error(salt_ssh_cli):
"""
Verify salt-ssh exits with a non-zero exit code when
it cannot decode the output of a command.
"""
ret = salt_ssh_cli.run("whoops.test")
assert ret.returncode == EX_AGGREGATE
assert isinstance(ret.data, dict)
assert (
ret.data["stdout"]
== '{ "local": { "whoops": "hrhrhr" }Warning: Chaos is not a letter\n}'
)
assert ret.data["retcode"] == 0


@pytest.mark.usefixtures("invalid_return_exe_mod")
def test_retcode_invalid_return(salt_ssh_cli):
"""
Verify salt-ssh exits with a non-zero exit code when
the decoded command output is invalid.
"""
ret = salt_ssh_cli.run("whoopsiedoodle.test", "false")
assert ret.returncode == EX_AGGREGATE
assert isinstance(ret.data, dict)
assert ret.data["stdout"] == '{"no_local_key_present": "Chaos is a ladder though"}'
assert ret.data["retcode"] == 0


@pytest.mark.usefixtures("remote_exception_wrap_mod")
def test_wrapper_unwrapped_command_exception(salt_ssh_cli):
"""
Verify salt-ssh does not return unexpected exception output to wrapper modules.
"""
ret = salt_ssh_cli.run("check_exception.failure")
assert ret.data
assert "Probably got garbage" not in ret.data
assert ret.returncode == EX_AGGREGATE


@pytest.mark.usefixtures("remote_parsing_failure_wrap_mod", "invalid_json_exe_mod")
def test_wrapper_unwrapped_command_parsing_failure(salt_ssh_cli):
"""
Verify salt-ssh does not return unexpected unparsable output to wrapper modules.
"""
ret = salt_ssh_cli.run("check_parsing.failure", "whoops")
assert ret.data
assert "Probably got garbage" not in ret.data
assert ret.returncode == EX_AGGREGATE


@pytest.mark.usefixtures("remote_parsing_failure_wrap_mod", "invalid_return_exe_mod")
def test_wrapper_unwrapped_command_invalid_return(salt_ssh_cli):
"""
Verify salt-ssh does not return unexpected unparsable output to wrapper modules.
"""
ret = salt_ssh_cli.run("check_parsing.failure", "whoopsiedoodle")
assert ret.data
assert "Probably got garbage" not in ret.data
assert ret.returncode == EX_AGGREGATE
35 changes: 35 additions & 0 deletions tests/pytests/integration/ssh/test_mine.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,31 @@
]


@pytest.fixture(scope="module", autouse=True)
def pillar_tree(base_env_pillar_tree_root_dir):
top_file = """
base:
'localhost':
- mine
'127.0.0.1':
- mine
"""
mine_pillar_file = """
mine_functions:
disk.usage:
- c
"""
top_tempfile = pytest.helpers.temp_file(
"top.sls", top_file, base_env_pillar_tree_root_dir
)
mine_tempfile = pytest.helpers.temp_file(
"mine.sls", mine_pillar_file, base_env_pillar_tree_root_dir
)

with top_tempfile, mine_tempfile:
yield


@pytest.fixture(autouse=True)
def thin_dir(salt_ssh_cli):
try:
Expand Down Expand Up @@ -54,3 +79,13 @@ def test_mine_get(salt_ssh_cli, salt_minion, tgts):
assert set(ret.data) == exp
for id_ in exp:
assert ret.data[id_] is True


def test_ssh_mine_get_error(salt_ssh_cli):
"""
Test that a mine function returning an error is not
included in the output.
"""
ret = salt_ssh_cli.run("mine.get", "localhost", "disk.usage")
assert ret.returncode == 0
assert not ret.data

0 comments on commit e89daea

Please sign in to comment.