fix: decide uninstall success by exit status, not by keywords
uninstall_package judged success by searching apt/dnf/apk/pacman output
for failure words. That is guesswork in both directions: apt's commonest
failure ("E: Sub-process /usr/bin/dpkg returned an error code (1)") read
as success until the previous change, and a prerm that prints "Failed to
stop ..." while the removal completes still reads as failure. The exit
status is the answer the package manager actually gives, but every
command went through `_sudo(... || true)`, which throws it away.
Add `_sudo_status()`, which runs the command via `_sudo` followed by
`; echo __NETORK_RC=$?` and returns `(output, exit_status)` with the
marker stripped. The `|| true` of other `_sudo` callers is untouched:
they still want output rather than a status. The marker is matched only
on a line of its own with digits, so an echoed command line (literal
`$?`) is never mistaken for it. If the marker never arrives the status
is None -- unknown, not success.
uninstall_package and its dpkg fallback now use it, and
`_uninstall_failed(output, rc)` lets rc decide whenever it is known,
falling back to the keyword check only when it is not.
Behaviour change worth knowing: removing a package that is not installed
exits 0 on apt (and dnf), so it now reports success where the keyword
"is not installed" used to report failure. The package is absent
afterwards, which is what the caller asked for, and netOrk dropping it
from the installed record is then correct.
Refs christianmanivong/netork#267
This commit is contained in:
@@ -984,3 +984,164 @@ class TestUninstallPackage:
|
||||
|
||||
assert result["success"] is True
|
||||
assert "apk del" in driver._device.send_command.call_args[0][0]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# uninstall_package – success from the exit status, not from prose (netork#267)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _with_rc(output: str, rc: int) -> str:
|
||||
"""What the shell prints for a command run through ``_sudo_status``."""
|
||||
return f"{output}\n__NETORK_RC={rc}"
|
||||
|
||||
|
||||
class TestSudoStatus:
|
||||
"""``_sudo_status`` keeps the exit status that ``|| true`` throws away."""
|
||||
|
||||
def test_returns_output_and_exit_status(self, driver):
|
||||
_mock_send(driver, _with_rc("Removing wazuh-agent ...", 0))
|
||||
|
||||
assert driver._sudo_status("apt-get remove -y wazuh-agent") == (
|
||||
"Removing wazuh-agent ...",
|
||||
0,
|
||||
)
|
||||
|
||||
def test_a_non_zero_exit_status_is_reported(self, driver):
|
||||
_mock_send(driver, _with_rc("E: Unable to locate package nope", 100))
|
||||
|
||||
assert driver._sudo_status("apt-get remove -y nope")[1] == 100
|
||||
|
||||
def test_the_status_is_read_right_after_sudo_returns(self, driver):
|
||||
"""``$?`` must be read straight after the sudo pipeline — with an
|
||||
``|| true`` in between, every command would report 0."""
|
||||
driver._sudo_password = "pw" # noqa: S105
|
||||
_mock_send(driver, _with_rc("", 0))
|
||||
|
||||
driver._sudo_status("apt-get remove -y x 2>&1")
|
||||
|
||||
sent = driver._device.send_command.call_args[0][0]
|
||||
assert sent.startswith("echo pw | sudo -S")
|
||||
assert sent.endswith("apt-get remove -y x 2>&1; echo __NETORK_RC=$?")
|
||||
assert "|| true" not in sent
|
||||
|
||||
def test_a_missing_marker_means_unknown_not_success(self, driver):
|
||||
"""Output cut short before the marker arrived says nothing about the
|
||||
exit status; ``None`` says so instead of guessing 0."""
|
||||
_mock_send(driver, "Removing wazuh-agent ...")
|
||||
|
||||
assert driver._sudo_status("apt-get remove -y wazuh-agent") == (
|
||||
"Removing wazuh-agent ...",
|
||||
None,
|
||||
)
|
||||
|
||||
def test_the_command_echo_is_not_mistaken_for_the_marker(self, driver):
|
||||
"""A terminal may echo the command line back; its literal ``$?`` is not
|
||||
a number, and only the marker on a line of its own counts."""
|
||||
_mock_send(
|
||||
driver,
|
||||
"sudo apt-get remove -y x; echo __NETORK_RC=$?\nRemoving x ...\n__NETORK_RC=1",
|
||||
)
|
||||
|
||||
output, rc = driver._sudo_status("apt-get remove -y x")
|
||||
|
||||
assert rc == 1
|
||||
assert "__NETORK_RC=1" not in output
|
||||
|
||||
|
||||
class TestUninstallExitStatus:
|
||||
"""Whether a removal worked is what the package manager's exit status says.
|
||||
|
||||
Reading it out of human-readable output was guesswork in both directions:
|
||||
apt's commonest failure (``E: Sub-process /usr/bin/dpkg returned an error
|
||||
code (1)``) read as success until #240, and a successful removal whose
|
||||
prerm merely *mentions* a failure read as a failure.
|
||||
"""
|
||||
|
||||
def test_a_non_zero_exit_is_a_failure_whatever_the_output_says(self, driver):
|
||||
"""Nothing in this output matches a failure keyword; only the exit
|
||||
status knows."""
|
||||
driver._pkg_manager = "dnf"
|
||||
_mock_send(driver, _with_rc("Removing: wazuh-agent", 1))
|
||||
|
||||
result = driver.uninstall_package("wazuh-agent")
|
||||
|
||||
assert result["success"] is False
|
||||
|
||||
def test_a_zero_exit_is_a_success_even_if_the_output_mentions_failure(self, driver):
|
||||
"""A prerm that cannot stop an already-dead unit prints "Failed" and
|
||||
still lets the removal complete."""
|
||||
_mock_send(
|
||||
driver,
|
||||
_with_rc(
|
||||
"Removing wazuh-agent (4.14.7-1) ...\n"
|
||||
"Failed to stop wazuh-agent.service: Unit wazuh-agent.service not loaded.",
|
||||
0,
|
||||
),
|
||||
)
|
||||
|
||||
result = driver.uninstall_package("wazuh-agent")
|
||||
|
||||
assert result["success"] is True
|
||||
|
||||
def test_the_marker_does_not_reach_the_caller(self, driver):
|
||||
_mock_send(driver, _with_rc("Removing wazuh-agent ...", 0))
|
||||
|
||||
result = driver.uninstall_package("wazuh-agent")
|
||||
|
||||
assert result["output"] == "Removing wazuh-agent ..."
|
||||
|
||||
def test_the_uninstall_command_keeps_its_exit_status(self, driver):
|
||||
_mock_send(driver, _with_rc("Removing wazuh-agent ...", 0))
|
||||
|
||||
driver.uninstall_package("wazuh-agent")
|
||||
|
||||
sent = driver._device.send_command.call_args[0][0]
|
||||
assert "|| true" not in sent
|
||||
assert sent.endswith("; echo __NETORK_RC=$?")
|
||||
|
||||
def test_apt_failing_by_exit_status_falls_back_to_dpkg(self, driver):
|
||||
driver._device.send_command.side_effect = [
|
||||
_with_rc("E: Sub-process /usr/bin/dpkg returned an error code (1)", 100),
|
||||
_with_rc("Removing wazuh-agent (4.14.7-1) ...", 0),
|
||||
]
|
||||
|
||||
result = driver.uninstall_package("wazuh-agent", purge=True)
|
||||
|
||||
assert result["success"] is True
|
||||
second = driver._device.send_command.call_args_list[1][0][0]
|
||||
assert "dpkg --purge --force-all" in second
|
||||
assert "|| true" not in second
|
||||
assert "__NETORK_RC" not in result["output"]
|
||||
|
||||
def test_the_dpkg_fallback_failing_is_a_failure(self, driver):
|
||||
driver._device.send_command.side_effect = [
|
||||
_with_rc("E: Sub-process /usr/bin/dpkg returned an error code (1)", 100),
|
||||
_with_rc("dpkg: error processing package wazuh-agent (--purge):", 1),
|
||||
]
|
||||
|
||||
result = driver.uninstall_package("wazuh-agent", purge=True)
|
||||
|
||||
assert result["success"] is False
|
||||
assert "dpkg --purge --force-all" in result["output"]
|
||||
|
||||
def test_a_zero_exit_does_not_trigger_the_fallback(self, driver):
|
||||
"""Even when the output contains words that used to mean failure: apt
|
||||
exits 0 for a package that is already gone, which is the state the
|
||||
caller asked for."""
|
||||
_mock_send(driver, _with_rc("Package 'x' is not installed, so not removed", 0))
|
||||
|
||||
result = driver.uninstall_package("x", purge=True)
|
||||
|
||||
assert result["success"] is True
|
||||
assert driver._device.send_command.call_count == 1
|
||||
|
||||
def test_without_an_exit_status_the_output_is_read_as_before(self, driver):
|
||||
"""If the marker never arrived, the keyword check is still the best
|
||||
answer available — and it errs towards failure on apt's ``E:``."""
|
||||
driver._pkg_manager = "dnf"
|
||||
_mock_send(driver, "E: Sub-process /usr/bin/dpkg returned an error code (1)")
|
||||
|
||||
result = driver.uninstall_package("wazuh-agent")
|
||||
|
||||
assert result["success"] is False
|
||||
|
||||
Reference in New Issue
Block a user