Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow explicit reboot on transactional minions #643

Open
wants to merge 13 commits into
base: openSUSE/release/3006.0
Choose a base branch
from
31 changes: 29 additions & 2 deletions salt/modules/transactional_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -985,13 +985,40 @@ def call(function, *args, **kwargs):
else:
return local
except ValueError:
return {"result": False, "retcode": 1, "comment": ret_stdout}
local = {"result": False, "retcode": 1, "comment": ret_stdout}
return local
finally:
# Check if reboot is needed
if activate_transaction and pending_transaction():
if (activate_transaction and pending_transaction()) or (
not kwargs.get("test", False) and _user_specified_reboot(local, function)
):
reboot()


def _user_specified_reboot(local, function):
if function != "state.highstate" and function != "state.sls":
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like we need to cover all of these functions:

DELEGATION_MAP = {
"state.single": "transactional_update.single",
"state.sls": "transactional_update.sls",
"state.apply": "transactional_update.apply",
"state.highstate": "transactional_update.highstate",
}

Actually state.highstate is not widely used in Uyuni/SUMA, but mostly state.apply with no mods specified (as state.apply with no mods is acting as state.highstate)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, great catch - didn't want to maintain the list of functions in two places, so I pulled in the DELEGATION_MAP directly. WTYD?

return False

if not isinstance(local, dict):
return False

explicit_reboot_cmds = set(["reboot", "system.reboot"])
explicit_reboot_modules = ["cmd_", "module_"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it's a bit wrong to specify here _ as a part of the module name, actually _|- is used as a separator in the state apply results like you use in the tests:

            "cmd_|-reboot_test_|-reboot_|-run": {
                "name": "reboot",
                "changes": {},
                "result": True,
            }

I don't know why but actually, cmd_|-reboot_test_|-reboot_|-run is a module name, id, name, fun separated with _|-, so maybe it's better to parse it using _|- instead of relying on |, but there is a mess that actually either the name or id can also contain _|- and it could add the mess, but even existing code in salt is producing wrong output in such cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense,thank you for the suggestion! Implemented.

names = set()
for full_module_name, module_result in local.items():
if not isinstance(full_module_name, str):
continue

module = full_module_name.split("|")[0]
m-czernek marked this conversation as resolved.
Show resolved Hide resolved
if module not in explicit_reboot_modules:
continue

if isinstance(module_result, dict) and "name" in module_result:
names.add(module_result["name"])

return bool(explicit_reboot_cmds.intersection(names))


def apply_(mods=None, **kwargs):
"""Apply an state inside a transaction.

Expand Down
139 changes: 136 additions & 3 deletions tests/pytests/unit/modules/test_transactional_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,9 +178,11 @@ def test_commands_with_global_params():
"--non-interactive",
"--drop-if-no-change",
"--no-selfupdate",
cmd.replace("_", ".")
if cmd.startswith("grub")
else cmd.replace("_", "-"),
(
cmd.replace("_", ".")
if cmd.startswith("grub")
else cmd.replace("_", "-")
),
]
)

Expand Down Expand Up @@ -501,6 +503,137 @@ def test_call_success_parameters():
)


def test_call_success_explicit_reboot():
"""Test transactional_update.call executes reboot when user specifies 'reboot' in sls"""
return_json = {
"local": {
"cmd_|-reboot_test_|-reboot_|-run": {
"name": "reboot",
"changes": {},
"result": True,
}
}
}
utils_mock = {
"json.find_json": MagicMock(return_value=return_json),
}
salt_mock = {
"cmd.run_all": MagicMock(return_value={"retcode": 0, "stdout": ""}),
}
reboot_mock = MagicMock()
with patch.dict(tu.__utils__, utils_mock), patch.dict(
tu.__salt__, salt_mock
), patch("salt.modules.transactional_update.reboot", reboot_mock):
tu.call("state.sls", key="value")
reboot_mock.assert_called_once()


def test_call_fail_explicit_reboot_test():
"""Test transactional_update.call does NOT execute reboot when user specifies 'reboot' in sls in test mode"""
return_json = {
"local": {
"cmd_|-reboot_test_|-reboot_|-run": {
"name": "reboot",
"changes": {},
"result": True,
}
}
}
utils_mock = {
"json.find_json": MagicMock(return_value=return_json),
}
salt_mock = {
"cmd.run_all": MagicMock(return_value={"retcode": 0, "stdout": ""}),
}
reboot_mock = MagicMock()
with patch.dict(tu.__utils__, utils_mock), patch.dict(
tu.__salt__, salt_mock
), patch("salt.modules.transactional_update.reboot", reboot_mock):
tu.call("state.sls", test="True")
assert not reboot_mock.called


def test_call_fail_explicit_reboot():
"""Test transactional_update.call does NOT execute reboot when the word 'reboot' appears in sls"""
return_json = {
"local": {
"cmd_|-reboot_test_|-reboot_|-run": {
"name": "service reboot",
"changes": {},
"result": True,
}
}
}
utils_mock = {
"json.find_json": MagicMock(return_value=return_json),
}
salt_mock = {
"cmd.run_all": MagicMock(return_value={"retcode": 0, "stdout": ""}),
}
reboot_mock = MagicMock()
with patch.dict(tu.__utils__, utils_mock), patch.dict(
tu.__salt__, salt_mock
), patch("salt.modules.transactional_update.reboot", reboot_mock):
tu.call("state.sls", test="True")
assert not reboot_mock.called


def test_call_fail_explicit_reboot_non_sls():
"""
Test transactional_update.call does NOT execute reboot when the word 'reboot'
appears in sls executed with function other than state.sls
"""
return_json = {
"local": {
"cmd_|-reboot_test_|-reboot_|-run": {
"name": "reboot",
"changes": {},
"result": True,
}
}
}
utils_mock = {
"json.find_json": MagicMock(return_value=return_json),
}
salt_mock = {
"cmd.run_all": MagicMock(return_value={"retcode": 0, "stdout": ""}),
}
reboot_mock = MagicMock()
with patch.dict(tu.__utils__, utils_mock), patch.dict(
tu.__salt__, salt_mock
), patch("salt.modules.transactional_update.reboot", reboot_mock):
tu.call("state.test")
assert not reboot_mock.called


def test_call_fail_explicit_reboot_non_cmd():
"""
Test transactional_update.call does NOT execute reboot when the word 'reboot'
appears in sls executed with module other than `cmd` or `module.include`
"""
return_json = {
"local": {
"test_|-reboot_test_|-reboot_|-run": {
"name": "reboot",
"changes": {},
"result": True,
}
}
}
utils_mock = {
"json.find_json": MagicMock(return_value=return_json),
}
salt_mock = {
"cmd.run_all": MagicMock(return_value={"retcode": 0, "stdout": ""}),
}
reboot_mock = MagicMock()
with patch.dict(tu.__utils__, utils_mock), patch.dict(
tu.__salt__, salt_mock
), patch("salt.modules.transactional_update.reboot", reboot_mock):
tu.call("state.sls")
assert not reboot_mock.called


def test_sls():
"""Test transactional_update.sls"""
salt_mock = {
Expand Down