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:
Christian Manivong
2026-09-25 14:40:26 +02:00
parent b4e6bbf79f
commit ac288823a7
2 changed files with 215 additions and 21 deletions
+161
View File
@@ -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