Agent 1.21.0. The hourly update check now covers the package managers beyond apt and dnf (#5803 "The agent's update check reads pacman, zypper and yum"): - yum: `yum -C check-update`, `updateinfo list security`, `needs-restarting -r`, as dnf. - zypper: `--no-refresh list-updates` (`--dup` on Tumbleweed), needed security patches from `list-patches --category security`, and `zypper needs-rebooting`. - pacman: `checkupdates` from pacman-contrib, which install-deps.sh now installs. pacman has no security notion; a reboot is required once the running kernel's /usr/lib/modules/<release> is gone. The checks sit in one UPDATE_CHECKS table, and a failure names its manager. The root helper's update runs on the same hosts (#5804 "The scheduled update runs on pacman, zypper and yum hosts"): yum upgrade, zypper refresh + update (dup on Tumbleweed, with 102/103 counted as success), pacman -Syu. Each step carries a label so a failure says which one, and the reboot check reads zypper and the pacman kernel too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -90,3 +90,118 @@ def test_a_reboot_is_required_when_debian_says_so(tmp_path):
|
||||
assert got["required"] is True
|
||||
assert got["packages"] == ["libc6", "linux-image-6.8.0-146-generic"]
|
||||
assert got["since"].startswith("2026-10-14")
|
||||
|
||||
|
||||
# ── pacman, zypper and yum (#5803) ───────────────────────────────────────────
|
||||
|
||||
CHECKUPDATES = ("linux 6.11.1.arch1-1 -> 6.11.2.arch1-1\n"
|
||||
"openssl 3.3.2-1 -> 3.4.0-1\n"
|
||||
"python-requests 2.32.3-1 -> 2.32.3-2\n")
|
||||
|
||||
ZYPPER_UPDATES = (
|
||||
"S | Repository | Name | Current Version | Available Version | Arch\n"
|
||||
"--+-----------------+-------------+---------------------+---------------------+-------\n"
|
||||
"v | repo-sle-update | libopenssl3 | 3.1.4-150600.5.7.1 | 3.1.4-150600.5.10.1 | x86_64\n"
|
||||
"v | repo-oss | vim | 9.1.0330-1.1 | 9.1.0698-1.1 | x86_64\n")
|
||||
|
||||
ZYPPER_PATCHES = (
|
||||
"Repository | Name | Category | Severity | Interactive | Status | Since | Summary\n"
|
||||
"----------------+-----------------------------+----------+-----------+-------------+------------+-------+--------\n"
|
||||
"repo-sle-update | openSUSE-SLE-15.6-2024-3601 | security | important | --- | needed | - | Security update for openssl-3\n"
|
||||
"repo-sle-update | openSUSE-SLE-15.6-2024-3500 | security | moderate | --- | applied | - | Security update for curl\n"
|
||||
"repo-sle-update | openSUSE-SLE-15.6-2024-3488 | security | low | --- | not needed | - | Security update for ruby\n")
|
||||
|
||||
YUM_CHECK_UPDATE = ("Loaded plugins: fastestmirror\n\n"
|
||||
"kernel.x86_64 3.10.0-1160.119.1.el7 updates\n"
|
||||
"openssl.x86_64 1:1.0.2k-26.el7_9 updates\n")
|
||||
|
||||
|
||||
def test_each_managers_list_is_counted():
|
||||
assert a.parse_checkupdates(CHECKUPDATES) == 3
|
||||
assert a.parse_checkupdates("") == 0
|
||||
assert a.parse_zypper_updates(ZYPPER_UPDATES) == 2
|
||||
assert a.parse_zypper_updates("No updates found.\n") == 0
|
||||
assert a.parse_zypper_patches(ZYPPER_PATCHES) == 1 # applied and not-needed aren't waiting
|
||||
assert a.parse_dnf_check_update(YUM_CHECK_UPDATE) == 2
|
||||
|
||||
|
||||
def _host(monkeypatch, answers: dict, binaries=()):
|
||||
"""A host whose commands answer from `answers` (keyed by the whole
|
||||
command, its first two words, or its first) and with `binaries` on the PATH."""
|
||||
calls = []
|
||||
|
||||
def fake_run(cmd, deadline=a.UPDATES_DEADLINE_SECONDS):
|
||||
calls.append(" ".join(cmd))
|
||||
for key in (" ".join(cmd), " ".join(cmd[:2]), cmd[0]):
|
||||
if key in answers:
|
||||
return answers[key]
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(a, "_run", fake_run)
|
||||
monkeypatch.setattr(a.shutil, "which", lambda name: f"/usr/bin/{name}" if name in binaries else None)
|
||||
return calls
|
||||
|
||||
|
||||
def test_a_pacman_host_is_checked_with_checkupdates(monkeypatch):
|
||||
_host(monkeypatch, {"checkupdates": (0, CHECKUPDATES, "")}, binaries=("pacman",))
|
||||
got = a.collect_updates()
|
||||
assert (got["manager"], got["pending"], got["security"]) == ("pacman", 3, None)
|
||||
|
||||
_host(monkeypatch, {"checkupdates": (2, "", "")}, binaries=("pacman",)) # 2: up to date
|
||||
assert a.collect_updates()["pending"] == 0
|
||||
|
||||
|
||||
def test_a_pacman_host_without_checkupdates_says_what_is_missing(monkeypatch):
|
||||
_host(monkeypatch, {}, binaries=("pacman",))
|
||||
got = a.collect_updates()
|
||||
assert got["manager"] == "pacman" and "pacman-contrib" in got["error"]
|
||||
|
||||
|
||||
def test_a_zypper_host_reads_updates_security_patches_and_reboot(monkeypatch):
|
||||
monkeypatch.setattr(a, "_os_release_id", lambda path="/etc/os-release": "opensuse-leap")
|
||||
_host(monkeypatch, {
|
||||
"zypper --no-refresh -q list-updates": (0, ZYPPER_UPDATES, ""),
|
||||
"zypper --no-refresh -q list-patches --category security": (0, ZYPPER_PATCHES, ""),
|
||||
"zypper needs-rebooting": (102, "", "")}) # 102: a reboot is needed
|
||||
got = a.collect_updates()
|
||||
assert (got["manager"], got["pending"], got["security"]) == ("zypper", 2, 1)
|
||||
assert got["reboot_required"] is True
|
||||
|
||||
|
||||
def test_tumbleweed_counts_what_dup_would_change_and_has_no_security_patches(monkeypatch):
|
||||
monkeypatch.setattr(a, "_os_release_id", lambda path="/etc/os-release": "opensuse-tumbleweed")
|
||||
calls = _host(monkeypatch, {"zypper --no-refresh": (0, ZYPPER_UPDATES, ""),
|
||||
"zypper needs-rebooting": (0, "", "")})
|
||||
got = a.collect_updates()
|
||||
assert got["pending"] == 2 and got["security"] is None and got["reboot_required"] is False
|
||||
assert "zypper --no-refresh -q list-updates --dup" in calls
|
||||
assert not any("list-patches" in c for c in calls)
|
||||
|
||||
|
||||
def test_a_yum_host_without_dnf_is_checked_like_dnf(monkeypatch):
|
||||
_host(monkeypatch, {"yum -q": (100, YUM_CHECK_UPDATE, ""), "needs-restarting -r": (1, "", "")})
|
||||
got = a.collect_updates()
|
||||
assert (got["manager"], got["pending"]) == ("yum", 2)
|
||||
assert got["reboot_required"] is True
|
||||
|
||||
|
||||
def test_a_failed_check_names_its_manager(monkeypatch):
|
||||
_host(monkeypatch, {"zypper --no-refresh": (7, "", "System management is locked by PackageKit")})
|
||||
monkeypatch.setattr(a, "_os_release_id", lambda path="/etc/os-release": "sles")
|
||||
got = a.collect_updates()
|
||||
assert got["manager"] == "zypper" and "locked" in got["error"]
|
||||
|
||||
|
||||
def test_a_pacman_host_needs_a_reboot_once_its_running_kernel_is_replaced(monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(a.shutil, "which", lambda name: "/usr/bin/pacman" if name == "pacman" else None)
|
||||
modules = tmp_path / "modules"
|
||||
(modules / os.uname().release).mkdir(parents=True)
|
||||
no_marker = str(tmp_path / "reboot-required")
|
||||
assert a.collect_reboot(no_marker, str(modules)) == {"required": False}
|
||||
(modules / os.uname().release).rmdir() # the upgrade removed it
|
||||
assert a.collect_reboot(no_marker, str(modules)) == {"required": True}
|
||||
|
||||
|
||||
def test_a_host_with_no_reboot_marker_to_read_says_nothing(monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(a.shutil, "which", lambda name: None)
|
||||
assert a.collect_reboot(str(tmp_path / "reboot-required")) is None
|
||||
|
||||
@@ -173,3 +173,85 @@ async def test_steward_serves_only_the_listed_files(app):
|
||||
for name in ("../agent.py", "agent.py", "routes.py", "../../steward/config.py"):
|
||||
_, status = await agent_file(name)
|
||||
assert status == 404, name
|
||||
|
||||
|
||||
# ── pacman, zypper and yum hosts (#5804) ─────────────────────────────────────
|
||||
|
||||
def test_the_helper_counts_what_zypper_and_pacman_changed(maint):
|
||||
zypper = ("The following 15 packages are going to be upgraded:\n bash vim ...\n"
|
||||
"15 packages to upgrade, 2 new.\nOverall download size: 48.2 MiB.\n")
|
||||
assert maint.upgraded_count(zypper) == 17
|
||||
pacman = ("resolving dependencies...\nlooking for conflicting packages...\n\n"
|
||||
"Packages (12) linux-6.11.2.arch1-1 openssl-3.4.0-1 ...\n\n"
|
||||
"Total Download Size: 152.31 MiB\n")
|
||||
assert maint.upgraded_count(pacman) == 12
|
||||
yum = "Transaction Summary\n====\nUpgrade 4 Packages\n"
|
||||
assert maint.upgraded_count(yum) == 4
|
||||
|
||||
|
||||
def _manager(monkeypatch, maint, binaries):
|
||||
monkeypatch.setattr(maint.shutil, "which",
|
||||
lambda name: f"/usr/bin/{name}" if name in binaries else None)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("binaries, manager", [
|
||||
(("apt-get",), "apt"), (("dnf", "yum"), "dnf"), (("yum",), "yum"),
|
||||
(("zypper",), "zypper"), (("pacman",), "pacman"), ((), None)])
|
||||
def test_the_helper_picks_the_hosts_package_manager(maint, monkeypatch, binaries, manager):
|
||||
_manager(monkeypatch, maint, binaries)
|
||||
assert maint.host_manager() == manager
|
||||
|
||||
|
||||
def test_each_manager_upgrades_without_asking(maint, monkeypatch):
|
||||
commands = {m: [step[1] for step in maint.update_steps(m)]
|
||||
for m in ("apt", "dnf", "yum", "pacman")}
|
||||
assert commands["pacman"] == [["pacman", "-Syu", "--noconfirm"]]
|
||||
assert commands["yum"] == [["yum", "-y", "update"]]
|
||||
assert commands["dnf"] == [["dnf", "-y", "upgrade", "--refresh"]]
|
||||
assert commands["apt"][-1][-1] == "dist-upgrade"
|
||||
monkeypatch.setattr(maint, "_os_release_id", lambda path="/etc/os-release": "opensuse-leap")
|
||||
leap = [step[1] for step in maint.update_steps("zypper")]
|
||||
assert leap == [["zypper", "--non-interactive", "refresh"],
|
||||
["zypper", "--non-interactive", "update", "--auto-agree-with-licenses"]]
|
||||
monkeypatch.setattr(maint, "_os_release_id", lambda path="/etc/os-release": "opensuse-tumbleweed")
|
||||
assert maint.update_steps("zypper")[-1][1][2] == "dup" # a rolling release upgrades with dup
|
||||
|
||||
|
||||
def test_a_zypper_update_that_asks_for_a_reboot_still_worked(maint, monkeypatch):
|
||||
"""zypper exits 102 when the update needs a reboot: done, not failed."""
|
||||
_manager(monkeypatch, maint, ("zypper",))
|
||||
monkeypatch.setattr(maint, "_os_release_id", lambda path="/etc/os-release": "sles")
|
||||
ran = []
|
||||
|
||||
def fake_run(cmd, deadline=0, env=None, ok_codes=(0,)):
|
||||
ran.append(cmd[2])
|
||||
code = 102 if cmd[2] == "update" else 0
|
||||
return code in ok_codes, "3 packages to upgrade.\n"
|
||||
|
||||
monkeypatch.setattr(maint, "run", fake_run)
|
||||
monkeypatch.setattr(maint, "reboot_required", lambda: True)
|
||||
got = maint.do_update()
|
||||
assert got["ok"] and got["packages"] == 3 and got["reboot_required"] is True
|
||||
assert ran == ["refresh", "update"]
|
||||
|
||||
|
||||
def test_a_failed_step_says_which(maint, monkeypatch):
|
||||
_manager(monkeypatch, maint, ("pacman",))
|
||||
monkeypatch.setattr(maint, "run", lambda cmd, **kw: (False, "error: failed to synchronize all databases"))
|
||||
got = maint.do_update()
|
||||
assert not got["ok"] and got["detail"].startswith("pacman -Syu failed: error: failed to synchronize")
|
||||
|
||||
|
||||
def test_a_host_with_no_package_manager_it_knows_says_so(maint, monkeypatch):
|
||||
_manager(monkeypatch, maint, ())
|
||||
assert "apt, dnf, yum, zypper, pacman" in maint.do_update()["detail"]
|
||||
|
||||
|
||||
def test_a_pacman_host_needs_a_reboot_once_the_running_kernel_is_replaced(maint, monkeypatch, tmp_path):
|
||||
_manager(monkeypatch, maint, ("pacman",))
|
||||
monkeypatch.setattr(maint, "REBOOT_MARKER", str(tmp_path / "reboot-required")) # none
|
||||
monkeypatch.setattr(maint, "KERNEL_MODULES", str(tmp_path))
|
||||
(tmp_path / os.uname().release).mkdir()
|
||||
assert maint.reboot_required() is False
|
||||
(tmp_path / os.uname().release).rmdir()
|
||||
assert maint.reboot_required() is True
|
||||
|
||||
Reference in New Issue
Block a user