diff --git a/plugins/host_agent/agent.py b/plugins/host_agent/agent.py index ed6a9e4..bbb6c5b 100644 --- a/plugins/host_agent/agent.py +++ b/plugins/host_agent/agent.py @@ -22,7 +22,7 @@ from collections import deque from concurrent.futures import ThreadPoolExecutor, wait as wait_futures from datetime import datetime, timezone -AGENT_VERSION = "1.26.0" +AGENT_VERSION = "1.26.1" # Default path to the local Docker Engine socket. Overridable via the # `docker_socket` config key; collection is silently skipped if it's absent or diff --git a/plugins/host_agent/files/maint.py b/plugins/host_agent/files/maint.py index a1d8e37..d0b45cf 100644 --- a/plugins/host_agent/files/maint.py +++ b/plugins/host_agent/files/maint.py @@ -101,10 +101,10 @@ def valid(req) -> bool: def run(cmd: list, deadline: int = COMMAND_DEADLINE_SECONDS, env: dict | None = None, - ok_codes: tuple = (0,), keep: int = OUTPUT_KEPT_CHARS): + ok_codes: tuple = (0,), keep: int | None = OUTPUT_KEPT_CHARS): """(ok, output tail) of a command, bounded by `deadline`. `ok_codes` are the exit codes that mean it worked (zypper says more than 0); `keep` is - how much of the output's end is kept.""" + how much of the output's end is kept, None for all of it.""" try: done = subprocess.run(cmd, capture_output=True, text=True, timeout=deadline, env=dict(os.environ, LANG="C", LC_ALL="C", **(env or {}))) @@ -113,7 +113,7 @@ def run(cmd: list, deadline: int = COMMAND_DEADLINE_SECONDS, env: dict | None = except OSError as exc: return False, str(exc) out = (done.stdout or "") + (done.stderr or "") - return done.returncode in ok_codes, out[-keep:] + return done.returncode in ok_codes, out if keep is None else out[-keep:] REBOOT_MARKER = "/var/run/reboot-required" # Debian and Ubuntu @@ -226,9 +226,11 @@ def do_update() -> dict: "(apt, dnf, yum, zypper, pacman) on this host"} out = "" for label, cmd, deadline, env, ok_codes in update_steps(manager): - ok, out = run(cmd, deadline=deadline, env=env, ok_codes=ok_codes) + # The whole output: every manager prints its count before the long + # log of what it then does, so a tail loses it on a large update. + ok, out = run(cmd, deadline=deadline, env=env, ok_codes=ok_codes, keep=None) if not ok: - return {"ok": False, "detail": f"{label} failed: " + out} + return {"ok": False, "detail": f"{label} failed: " + out[-OUTPUT_KEPT_CHARS:]} changed = upgraded_count(out) if manager == "apt": run(["apt-get", "-y", "autoremove"], env={"DEBIAN_FRONTEND": "noninteractive"}) diff --git a/tests/plugins/host_agent/test_maint_helper.py b/tests/plugins/host_agent/test_maint_helper.py index 453e839..dbc6724 100644 --- a/tests/plugins/host_agent/test_maint_helper.py +++ b/tests/plugins/host_agent/test_maint_helper.py @@ -270,7 +270,7 @@ def test_a_zypper_update_that_asks_for_a_reboot_still_worked(maint, monkeypatch) monkeypatch.setattr(maint, "_os_release_id", lambda path="/etc/os-release": "sles") ran = [] - def fake_run(cmd, deadline=0, env=None, ok_codes=(0,)): + def fake_run(cmd, deadline=0, env=None, ok_codes=(0,), keep=0): ran.append(cmd[2]) code = 102 if cmd[2] == "update" else 0 return code in ok_codes, "3 packages to upgrade.\n" @@ -282,6 +282,20 @@ def test_a_zypper_update_that_asks_for_a_reboot_still_worked(maint, monkeypatch) assert ran == ["refresh", "update"] +def test_a_large_update_still_counts_its_packages(maint, monkeypatch): + """apt prints its count before the log of unpacking and setting up each + package, which runs far past what a detail keeps.""" + _manager(monkeypatch, maint, ("apt-get",)) + log = "17 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.\n" + \ + "Setting up libfoo (1.2-3) ...\n" * 200 + monkeypatch.setattr(maint, "run", lambda cmd, **kw: (True, log if kw.get("keep", 0) is None + else log[-maint.OUTPUT_KEPT_CHARS:])) + monkeypatch.setattr(maint, "reboot_required", lambda: False) + got = maint.do_update() + assert got["ok"] and got["packages"] == 17 + assert len(got["detail"]) <= 500 + + 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"))