feat: report how full /, /boot and /var are in the host status
CI / test (3.10) (push) Successful in 26s
CI / test (3.11) (push) Successful in 23s
CI / test (3.12) (push) Successful in 23s
CI / test (3.10) (pull_request) Successful in 23s
CI / test (3.11) (pull_request) Successful in 26s
CI / test (3.12) (pull_request) Successful in 25s

A Proxmox host's root filesystem stood at 94 % with about 400 MB free for a
week; a kernel upgrade failed on it with "No space left on device" and left
dpkg halfway, and nothing had said the disk was full (NetOrk/netork#862).

HOST_STATUS_COMMAND now also runs POSIX `df -Pk / /boot /var`, without root.
get_host_status() returns `filesystems`: each filesystem once (a directory
that is not a mount of its own is the filesystem it lives on), with its
mountpoint, total, used and available bytes, or None when df said nothing.
`available` is what df says an ordinary user can still write, which on ext4
is less than total minus used. How full is too full is the caller's decision.

Version 4.4.0.
This commit is contained in:
christianmanivong committed 2026-10-11 09:06:00 +02:00
1 parent ff6dbe0206
commit c947250179
5 files changed
+134 -3

No files matched your search

+78
View File
@@ -428,3 +428,81 @@ class TestBusyCheck:
"""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)
class TestFilesystems:
"""How full the filesystems an upgrade writes to are: ``/``, ``/boot`` and
``/var``, read with ``df -Pk`` and no root.
pve-dual's root filesystem stood at 94 % with about 400 MB free for a week;
a kernel upgrade failed on it with "No space left on device" and left dpkg
halfway, and nothing had said the disk was full (NetOrk/netork#862). The
figures below are shaped after it (6.5 GiB, 400 MiB available): ext4 keeps
5 % for root, so what is available is less than total minus used.
"""
HEADER = "Filesystem 1024-blocks Used Available Capacity Mounted on"
PVE_ROOT = "/dev/mapper/pve-root 6815744 6065357 409600 94% /"
BOOT = "/dev/sda2 1046512 104860 941652 11% /boot"
@staticmethod
def _report(*df_lines: str) -> str:
return _wire().replace("[timers]", "\n".join(["[filesystems]", *df_lines, "[timers]"]))
def test_the_root_filesystem_in_bytes(self):
status = parse_host_status(self._report(self.HEADER, self.PVE_ROOT))
assert status["filesystems"] == [
{
"mountpoint": "/",
"total": 6815744 * 1024,
"used": 6065357 * 1024,
"available": 409600 * 1024,
}
]
def test_a_separate_boot_partition(self):
status = parse_host_status(self._report(self.HEADER, self.PVE_ROOT, self.BOOT))
assert [fs["mountpoint"] for fs in status["filesystems"]] == ["/", "/boot"]
def test_a_directory_on_root_is_root_once(self):
"""``df /boot /var`` names the filesystem they live on, here ``/`` three times."""
status = parse_host_status(
self._report(self.HEADER, self.PVE_ROOT, self.PVE_ROOT, self.PVE_ROOT)
)
assert [fs["mountpoint"] for fs in status["filesystems"]] == ["/"]
def test_a_long_device_name_and_a_blank_in_the_mountpoint(self):
line = "/dev/disk/by-id/a very long name 1000 600 400 60% /mnt/my disk"
(fs,) = parse_host_status(self._report(self.HEADER, line))["filesystems"]
assert (fs["mountpoint"], fs["available"]) == ("/mnt/my disk", 400 * 1024)
def test_a_filesystem_without_size_is_left_out(self):
"""What a pseudo filesystem reports; it cannot run full."""
status = parse_host_status(self._report(self.HEADER, "none 0 0 0 - /"))
assert status["filesystems"] is None
def test_without_the_section_it_is_unknown(self):
"""An older command, or no df: None, not "nothing is full"."""
assert parse_host_status(_wire())["filesystems"] is None
def test_df_that_printed_nothing_is_unknown(self):
assert parse_host_status(self._report())["filesystems"] is None
def test_the_command_asks_for_the_three_without_root(self):
assert "df -Pk / /boot /var" in HOST_STATUS_COMMAND
assert "sudo" not in HOST_STATUS_COMMAND
def test_the_command_reads_them_on_a_real_shell(self):
"""POSIX df the way a host's sh runs it: ``/`` is always there, whatever it is."""
out = subprocess.run(
["sh", "-c", HOST_STATUS_COMMAND], capture_output=True, text=True, timeout=30
)
mountpoints = [fs["mountpoint"] for fs in parse_host_status(out.stdout)["filesystems"]]
assert "/" in mountpoints