feat: report where an update comes from and whether it is a security fix, refresh the index, read the host status
For netOrk MVP 5, on napalm-device-types 2.3.0: - get_available_updates (apt) runs the shared APT_UPGRADABLE_COMMAND and parse_apt_upgradable: origin and security from apt's suites, and a ValueError instead of [] when apt failed or its output was cut short. - dnf/yum: check-update's exit status decides (0 none, 100 updates, anything else raises); security comes from `updateinfo list --security`, and is None when dnf cannot say. The repository column becomes the origin. - refresh_available_updates(): apt-get update, dnf/yum makecache, apk update; pacman is left out (-Sy without -u invites a partial upgrade). - HostStatusMixin: reboot required and self-patching, read over SSH. - _run_privileged(): root runs directly, a sudo password goes through _sudo, otherwise sudo -n. Shared by service control and the refresh. - _split_status() drops terminal codes before it looks for the exit status; a pseudo-terminal left keypad codes in front of the marker. OpenMediaVault inherits all of it.
This commit is contained in:
@@ -226,6 +226,7 @@ APT_UPGRADABLE = (
|
||||
"Listing... Done\n"
|
||||
"openssh-server/stable 1:9.2p1-2+deb12u2 amd64 [upgradable from: 1:9.2p1-2+deb12u1]\n"
|
||||
"curl/stable 7.88.1-10+deb12u6 amd64 [upgradable from: 7.88.1-10+deb12u5]\n"
|
||||
"__APT_RC=0\n"
|
||||
)
|
||||
|
||||
|
||||
@@ -1282,3 +1283,135 @@ class TestManageService:
|
||||
driver.manage_service("cron; reboot", "stop")
|
||||
|
||||
assert driver._device.send_command.call_count == 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Updates: origin and security, refresh, host status (netOrk MVP 5)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
APT_WITH_SECURITY = (
|
||||
"openssl/noble-updates,noble-security 3.0.13-0ubuntu3.6 amd64 [upgradable from: 3.0.13-0ubuntu3.5]\n"
|
||||
"docker-compose-plugin/noble 5.6.0-1~ubuntu.24.04~noble amd64 [upgradable from: 5.5.1-1~ubuntu.24.04~noble]\n"
|
||||
"__APT_RC=0\n"
|
||||
)
|
||||
|
||||
|
||||
class TestAvailableUpdates:
|
||||
def test_apt_reports_origin_and_security(self, driver):
|
||||
driver._pkg_manager = "apt"
|
||||
_mock_send(driver, APT_WITH_SECURITY)
|
||||
|
||||
updates = {u["name"]: u for u in driver.get_available_updates()}
|
||||
|
||||
assert updates["openssl"]["security"] is True
|
||||
assert updates["openssl"]["origin"] == "noble-updates,noble-security"
|
||||
assert updates["docker-compose-plugin"]["security"] is False
|
||||
|
||||
def test_apt_that_could_not_read_raises_instead_of_reporting_nothing(self, driver):
|
||||
driver._pkg_manager = "apt"
|
||||
_mock_send(driver, "E: Could not open lock file\n__APT_RC=100\n")
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
driver.get_available_updates()
|
||||
|
||||
def test_apt_without_an_exit_status_raises(self, driver):
|
||||
driver._pkg_manager = "apt"
|
||||
_mock_send(driver, "openssl/noble-security 3.0.13-0ubuntu3.6 amd64 [upgradable fro")
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
driver.get_available_updates()
|
||||
|
||||
def test_dnf_marks_what_a_security_advisory_covers(self, driver):
|
||||
driver._pkg_manager = "dnf"
|
||||
driver._device.send_command.side_effect = [
|
||||
"0", # id -u
|
||||
"openssl-libs.x86_64 1:3.1.4-2.fc40 updates\n"
|
||||
"vim-enhanced.x86_64 2:9.1.083-1.fc40 updates\n__NETORK_RC=100",
|
||||
"FEDORA-2024-1a2b3c4d5e Important/Sec. openssl-libs-1:3.1.4-2.fc40.x86_64\n__NETORK_RC=0",
|
||||
]
|
||||
|
||||
updates = {u["name"]: u for u in driver.get_available_updates()}
|
||||
|
||||
assert updates["openssl-libs"]["security"] is True
|
||||
assert updates["vim-enhanced"]["security"] is False
|
||||
|
||||
def test_dnf_without_advisories_leaves_security_unknown(self, driver):
|
||||
driver._pkg_manager = "dnf"
|
||||
driver._device.send_command.side_effect = [
|
||||
"0",
|
||||
"vim-enhanced.x86_64 2:9.1.083-1.fc40 updates\n__NETORK_RC=100",
|
||||
"Error: updateinfo metadata missing\n__NETORK_RC=1",
|
||||
]
|
||||
|
||||
assert driver.get_available_updates()[0]["security"] is None
|
||||
|
||||
def test_dnf_that_failed_raises(self, driver):
|
||||
driver._pkg_manager = "dnf"
|
||||
driver._device.send_command.side_effect = ["0", "Error: Failed to download metadata\n__NETORK_RC=1"]
|
||||
|
||||
with pytest.raises(RuntimeError):
|
||||
driver.get_available_updates()
|
||||
|
||||
|
||||
class TestRefreshAvailableUpdates:
|
||||
def _sent(self, driver) -> list:
|
||||
return [c[0][0] for c in driver._device.send_command.call_args_list]
|
||||
|
||||
def test_apt_refreshes_its_index_as_root(self, driver):
|
||||
driver._pkg_manager = "apt"
|
||||
driver._device.send_command.side_effect = ["0", "Hit:1 http://archive.ubuntu.com noble InRelease\n__NETORK_RC=0"]
|
||||
|
||||
result = driver.refresh_available_updates()
|
||||
|
||||
assert result["success"] is True
|
||||
assert "apt-get update" in self._sent(driver)[1]
|
||||
|
||||
def test_without_a_sudo_password_it_never_waits_for_one(self, driver):
|
||||
driver._pkg_manager = "apt"
|
||||
driver._device.send_command.side_effect = ["1000", "sudo: a password is required\n__NETORK_RC=1"]
|
||||
|
||||
result = driver.refresh_available_updates()
|
||||
|
||||
assert result["success"] is False
|
||||
assert self._sent(driver)[1].startswith("sudo -n apt-get update")
|
||||
|
||||
def test_dnf_refreshes_its_metadata(self, driver):
|
||||
driver._pkg_manager = "dnf"
|
||||
driver._device.send_command.side_effect = ["0", "Metadata cache created.\n__NETORK_RC=0"]
|
||||
|
||||
assert driver.refresh_available_updates()["success"] is True
|
||||
assert "dnf makecache" in self._sent(driver)[1]
|
||||
|
||||
def test_pacman_is_not_refreshed_on_its_own(self, driver):
|
||||
"""pacman -Sy without -u invites a partial upgrade on the next install."""
|
||||
driver._pkg_manager = "pacman"
|
||||
|
||||
result = driver.refresh_available_updates()
|
||||
|
||||
assert result["success"] is False
|
||||
driver._device.send_command.assert_not_called()
|
||||
|
||||
|
||||
class TestHostStatus:
|
||||
def test_the_driver_carries_the_shared_command(self, driver):
|
||||
from napalm_device_types.host_status import HOST_STATUS_COMMAND
|
||||
|
||||
_mock_send(
|
||||
driver,
|
||||
"HSTAT_BEGIN\n[reboot-required]\n[kernel]\n6.8.0-142-generic\n[modules]\n"
|
||||
"6.8.0-142-generic\n[timers]\napt-daily-upgrade.timer enabled\nHSTAT_END\n",
|
||||
)
|
||||
|
||||
status = driver.get_host_status()
|
||||
|
||||
assert driver._device.send_command.call_args[0][0] == HOST_STATUS_COMMAND
|
||||
assert status["reboot_required"] is True
|
||||
|
||||
|
||||
class TestTerminalCodes:
|
||||
def test_a_status_marker_behind_a_terminal_code_is_still_read(self, driver):
|
||||
"""apt-get on a pseudo-terminal leaves keypad codes in front of the marker."""
|
||||
driver._pkg_manager = "apt"
|
||||
driver._device.send_command.side_effect = ["0", "Hit:1 noble InRelease\n\x1b>__NETORK_RC=0"]
|
||||
|
||||
assert driver.refresh_available_updates()["success"] is True
|
||||
|
||||
Reference in New Issue
Block a user