mirror of
https://github.com/saltstack/salt.git
synced 2025-04-17 10:10:20 +00:00

- Fix thin dir unit tests after distro change revert - Skip any tests that require system python of 3.9 or higher
361 lines
11 KiB
Python
361 lines
11 KiB
Python
"""
|
|
salt-ssh testing
|
|
"""
|
|
|
|
import pathlib
|
|
import shutil
|
|
|
|
import pytest
|
|
|
|
import salt.utils.files
|
|
import salt.utils.yaml
|
|
from salt.defaults.exitcodes import EX_AGGREGATE
|
|
from tests.pytests.integration.ssh import check_system_python_version
|
|
|
|
pytestmark = [
|
|
pytest.mark.slow_test,
|
|
pytest.mark.skip_on_windows(reason="salt-ssh not available on Windows"),
|
|
pytest.mark.skipif(
|
|
not check_system_python_version(), reason="Needs system python >= 3.9"
|
|
),
|
|
]
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def thin_dir(salt_ssh_cli):
|
|
try:
|
|
yield
|
|
finally:
|
|
ret = salt_ssh_cli.run("config.get", "thin_dir")
|
|
assert ret.returncode == 0
|
|
thin_dir_path = ret.data
|
|
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
|
|
"""
|
|
ret = salt_ssh_cli.run("test.ping")
|
|
assert ret.returncode == 0
|
|
assert ret.data is True
|
|
|
|
|
|
def test_thin_dir(salt_ssh_cli):
|
|
"""
|
|
test to make sure thin_dir is created
|
|
and salt-call file is included
|
|
"""
|
|
ret = salt_ssh_cli.run("config.get", "thin_dir")
|
|
assert ret.returncode == 0
|
|
thin_dir = pathlib.Path(ret.data)
|
|
assert thin_dir.is_dir()
|
|
assert thin_dir.joinpath("salt-call").exists()
|
|
assert thin_dir.joinpath("running_data").exists()
|
|
|
|
|
|
def test_wipe(salt_ssh_cli):
|
|
"""
|
|
Ensure --wipe is respected by the state module wrapper
|
|
issue #61083
|
|
"""
|
|
ret = salt_ssh_cli.run("config.get", "thin_dir")
|
|
assert ret.returncode == 0
|
|
thin_dir = pathlib.Path(ret.data)
|
|
assert thin_dir.exists()
|
|
# only few modules (state and cp) will actually respect --wipe
|
|
# (see commit #8a414d53284ec04940540ebd823306ab5119e105)
|
|
salt_ssh_cli.run("--wipe", "state.apply")
|
|
assert not thin_dir.exists()
|
|
|
|
|
|
def test_set_path(salt_ssh_cli, tmp_path, salt_ssh_roster_file):
|
|
"""
|
|
test setting the path env variable
|
|
"""
|
|
path = "/pathdoesnotexist/"
|
|
roster_file = tmp_path / "roster-set-path"
|
|
with salt.utils.files.fopen(salt_ssh_roster_file) as rfh:
|
|
roster_data = salt.utils.yaml.safe_load(rfh)
|
|
roster_data["localhost"].update(
|
|
{
|
|
"set_path": f"$PATH:/usr/local/bin/:{path}",
|
|
}
|
|
)
|
|
with salt.utils.files.fopen(roster_file, "w") as wfh:
|
|
salt.utils.yaml.safe_dump(roster_data, wfh)
|
|
|
|
ret = salt_ssh_cli.run(f"--roster-file={roster_file}", "environ.get", "PATH")
|
|
assert ret.returncode == 0
|
|
assert path in ret.data
|
|
|
|
|
|
def test_tty(salt_ssh_cli, tmp_path, salt_ssh_roster_file):
|
|
"""
|
|
test using tty
|
|
"""
|
|
roster_file = tmp_path / "roster-tty"
|
|
with salt.utils.files.fopen(salt_ssh_roster_file) as rfh:
|
|
roster_data = salt.utils.yaml.safe_load(rfh)
|
|
roster_data["localhost"].update({"tty": True})
|
|
with salt.utils.files.fopen(roster_file, "w") as wfh:
|
|
salt.utils.yaml.safe_dump(roster_data, wfh)
|
|
ret = salt_ssh_cli.run(f"--roster-file={roster_file}", "test.ping")
|
|
assert ret.returncode == 0
|
|
assert ret.data is True
|
|
|
|
|
|
def test_retcode_exe_run_fail(salt_ssh_cli):
|
|
"""
|
|
Verify salt-ssh passes through the retcode it receives.
|
|
"""
|
|
ret = salt_ssh_cli.run("file.touch", "/tmp/non/ex/is/tent")
|
|
assert ret.returncode == EX_AGGREGATE
|
|
assert isinstance(ret.data, dict)
|
|
# This should be the exact output, but some other warnings
|
|
# might be printed to stderr.
|
|
assert "Error running 'file.touch': No such file or directory" in ret.data["stderr"]
|
|
|
|
|
|
def test_retcode_exe_run_exception(salt_ssh_cli):
|
|
"""
|
|
Verify salt-ssh passes through the retcode it receives
|
|
when an exception is thrown. (Ref #50727)
|
|
"""
|
|
ret = salt_ssh_cli.run("salttest.jinja_error")
|
|
assert ret.returncode == EX_AGGREGATE
|
|
assert isinstance(ret.data, dict)
|
|
assert ret.data["stderr"].endswith("Exception: hehehe")
|
|
|
|
|
|
@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["_error"] == "Failed to return clean data"
|
|
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["_error"] == "Return dict was malformed"
|
|
assert ret.data["retcode"] == 0
|
|
assert ret.data["parsed"] == {"no_local_key_present": "Chaos is a ladder though"}
|
|
|
|
|
|
@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.returncode == EX_AGGREGATE
|
|
# "Probably got garbage" would be returned as a string (the module return),
|
|
# so no need to check
|
|
assert isinstance(ret.data, dict)
|
|
assert ret.data
|
|
assert (
|
|
"Error running 'disk.usage': Invalid flag passed to disk.usage"
|
|
in ret.data["stderr"]
|
|
)
|
|
|
|
|
|
@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.returncode == EX_AGGREGATE
|
|
assert isinstance(ret.data, dict)
|
|
assert ret.data
|
|
assert ret.data["_error"] == "Failed to return clean data"
|
|
assert ret.data["retcode"] == 0
|
|
assert (
|
|
ret.data["stdout"]
|
|
== '{ "local": { "whoops": "hrhrhr" }Warning: Chaos is not a letter\n}'
|
|
)
|
|
|
|
|
|
@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.returncode == EX_AGGREGATE
|
|
assert isinstance(ret.data, dict)
|
|
assert ret.data
|
|
assert ret.data["_error"] == "Return dict was malformed"
|
|
assert ret.data["retcode"] == 0
|
|
assert (
|
|
ret.data["stdout"]
|
|
== '{"local": {"no_return_key_present": "Chaos is a ladder though"}}'
|
|
)
|
|
assert ret.data["parsed"] == {
|
|
"local": {"no_return_key_present": "Chaos is a ladder though"}
|
|
}
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def utils_dependent_module(salt_run_cli, salt_master):
|
|
module_contents = r"""
|
|
import customutilsmodule
|
|
|
|
|
|
def __virtual__():
|
|
return "utilsync"
|
|
|
|
|
|
def test():
|
|
return customutilsmodule.test()
|
|
"""
|
|
utils_contents = r"""
|
|
def test():
|
|
return "success"
|
|
"""
|
|
module_tempfile = salt_master.state_tree.base.temp_file(
|
|
"_modules/utilsync.py", contents=module_contents
|
|
)
|
|
util_tempfile = salt_master.state_tree.base.temp_file(
|
|
"_utils/customutilsmodule.py", contents=utils_contents
|
|
)
|
|
with module_tempfile, util_tempfile:
|
|
yield
|
|
|
|
|
|
@pytest.mark.usefixtures("utils_dependent_module")
|
|
def test_custom_utils_are_present_on_target(salt_ssh_cli):
|
|
ret = salt_ssh_cli.run("utilsync.test")
|
|
assert ret.returncode == 0
|
|
assert ret.data == "success"
|