fix: tell unattended-upgrade from Ubuntu's idle shutdown helper
CI / test (3.10) (push) Successful in 24s
CI / test (3.11) (push) Successful in 23s
CI / test (3.12) (push) Successful in 24s
CI / test (3.10) (pull_request) Successful in 24s
CI / test (3.11) (pull_request) Successful in 22s
CI / test (3.12) (pull_request) Successful in 25s

On every Ubuntu host with unattended-upgrades, get_host_status() read the
package manager as busy, so an interrupted dpkg was never reported there.
Ubuntu keeps unattended-upgrade-shutdown --wait-for-signal running all the
time; its process name is cut to 15 characters, unattended-upgr, the same
as a real run's.

BUSY_CHECK now matches dpkg, apt, apt-get and aptitude by name and
unattended-upgrade by its command line. The tests run it through sh with a
stand-in ps, and check that the command does not match itself: over an exec
channel it is the command line of the shell that runs it.

Closes #26
This commit is contained in:
2026-10-08 17:30:44 +02:00
parent 72679ff4f8
commit 7aa731ad34
3 changed files with 67 additions and 6 deletions
+53
View File
@@ -11,10 +11,14 @@ The fixtures are the real states of six hosts on netOrk's test server.
from __future__ import annotations
import re
import subprocess
import pytest
from napalm_device_types import OSDriver
from napalm_device_types.host_status import (
BUSY_CHECK,
HOST_STATUS_COMMAND,
HostStatusMixin,
kernel_reboot_pending,
@@ -375,3 +379,52 @@ class TestAutoUpdatesLastSuccess:
def test_the_command_reads_the_upgrade_stamp_only(self):
assert "/var/lib/apt/periodic/upgrade-stamp" in HOST_STATUS_COMMAND
assert "update-success-stamp" not in HOST_STATUS_COMMAND
class TestBusyCheck:
"""Whether a package manager is working right now, run through ``sh`` with a
stand-in ``ps``. On Ubuntu, unattended-upgrades keeps
``unattended-upgrade-shutdown --wait-for-signal`` running all the time; its
process name is cut to 15 characters, ``unattended-upgr``, the same as a real
run's. Taken for a run, it made every such host's dpkg state unknown -- aris
among them (NetOrk/netork#738)."""
UBUNTU_IDLE = [
("systemd", "/sbin/init"),
("unattended-upgr", "/usr/bin/python3 /usr/share/unattended-upgrades/unattended-upgrade-shutdown --wait-for-signal"),
("sshd", "sshd: netork [priv]"),
]
@staticmethod
def _busy(tmp_path, processes) -> bool:
comm = "\n".join(c for c, _ in processes)
args = "\n".join(a for _, a in processes)
ps = tmp_path / "ps"
ps.write_text(
"#!/bin/sh\n"
f"case \"$*\" in *comm=*) printf '%s\\n' '{comm}';; *) printf '%s\\n' '{args}';; esac\n"
)
ps.chmod(0o755)
env = {"PATH": f"{tmp_path}:/usr/bin:/bin"}
out = subprocess.run(["sh", "-c", BUSY_CHECK], capture_output=True, text=True, env=env)
return "[dpkg-busy]" in out.stdout
def test_the_shutdown_helper_of_an_idle_ubuntu_host_is_no_run(self, tmp_path):
assert self._busy(tmp_path, self.UBUNTU_IDLE) is False
def test_unattended_upgrade_running(self, tmp_path):
run = ("unattended-upgr", "/usr/bin/python3 /usr/bin/unattended-upgrade")
assert self._busy(tmp_path, [*self.UBUNTU_IDLE, run]) is True
@pytest.mark.parametrize("name", ["dpkg", "apt", "apt-get", "aptitude"])
def test_dpkg_and_apt(self, tmp_path, name):
assert self._busy(tmp_path, [*self.UBUNTU_IDLE, (name, f"/usr/bin/{name} upgrade")]) is True
def test_it_is_part_of_the_command(self):
assert BUSY_CHECK in HOST_STATUS_COMMAND
def test_the_command_does_not_match_itself(self):
"""Run over an exec channel, the command is the command line of the shell
that runs it, and ``ps -o args`` lists it."""
assert not re.search(r"(^|[ /])unattended-upgrade( |$)", HOST_STATUS_COMMAND)