feat(commons): reclaim storage on a guest whose disk is already full - #28
Merged
Conversation
Host-agnostic, point it at any inventory. It removes only what is regenerable
(apt cache, package lists) or already rotated (journal segments, *.gz logs).
THE FIRST PLAY IS THE POINT, AND IT IS WHY THIS IS NOT A THREE-TASK PLAYBOOK.
A full root breaks Ansible before it breaks anything else: every module -
including the implicit `setup` - is copied into remote_tmp (/tmp, per
ansible.cfg) before it can run, so at 100% the play dies at task one with
mkdir: cannot create directory '/tmp/ansible-tmp-...': No space left on device
fatal: [host]: UNREACHABLE!
which reads as a connectivity failure and is not. That play therefore uses
ansible.builtin.raw and nothing else, with gather_facts disabled: raw goes
straight down the SSH channel and needs neither a temp directory nor Python. It
frees the journal and the apt cache - both pure deletes, which is what makes
them work at zero bytes free - and hands a usable filesystem to the second play.
When even that is not enough it stops with the reason rather than letting the
next play repeat the UNREACHABLE.
Two guards earn their place, both found by running this against cloudflared_vm:
- An apt-lock pre-check. ansible.builtin.apt waits lock_timeout and then fails
with a message naming neither the holder nor its age, and it aborts the
play - so the log reclaim never reaches the host that needed it. It now
skips apt, prints the holder with elapsed time, and continues. The holder
there had been wedged for 61 days.
- A superseded-kernel release. autoremove only takes packages marked `auto`,
so a manually-marked kernel is invisible to the one tool meant to find it.
Releasing them hands the decision back to apt's retention rules rather than
removing anything here; the running and newest kernels are excluded and
asserted after.
The second guard carries the trap that made this hard to measure: dpkg-query -W
lists `rc` packages - removed, config files remaining - beside installed ones,
each with a plausible Installed-Size. Counting those is how a guest with two
kernels reads as a guest with fourteen. The apt-mark showmanual intersection is
what excludes them.
Also reports REBOOT PENDING when the running kernel is not the newest installed,
because autoremove keeps both and the pile only collapses on the reboot that
makes them the same package - the difference between "found nothing" and
"cannot help until you reboot".
Verified end to end on cloudflared_vm: 100% used and UNREACHABLE, to 77% used
with 678 MiB free. ansible-lint passes at the production profile; yamllint,
typos and lychee clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Host-agnostic — point it at any inventory in this tree.
Why the first play uses
rawand nothing elseThis is the failure that motivated it, and it does not look like a disk problem:
UNREACHABLEreads as SSH or connectivity. It is neither. Ansible copies everymodule — including the implicit
setup— intoremote_tmpbefore it can runit, and
ansible.cfgsets that to/tmp. At 100% there is nowhere to put it, sothe run dies before task one and no ordinary playbook can help.
So play one is
ansible.builtin.rawonly, withgather_facts: false.rawrunsstraight down the SSH channel and needs neither a temp directory nor Python. It
frees the journal and the apt cache — both pure deletes, which is what makes them
work at zero bytes free — and hands a usable filesystem to play two. If even that
is not enough it stops with the reason rather than repeating the
UNREACHABLE.What it removes
.debis re-downloadableautoremove --purge*.gz,*.xz,*.zst,*.1, … under/var/log/tmp,/var/tmpTags:
journal,apt,logs,tmp,docker,kernels. Every default is aplay var.
Two guards, both found by running it for real
apt-lock pre-check.
ansible.builtin.aptwaitslock_timeoutthen fails witha message naming neither the holder nor its age — and it aborts the play, so the
log reclaim never reaches the host that needed it most. It now skips apt, prints
the holder with elapsed time, and continues. On
cloudflared_vmthe holder was anapt-get -qq -y updatefromapt.systemd.dailywedged for 61 days.Superseded-kernel release.
autoremoveonly takes packages markedauto, so amanually-marked kernel is invisible to the one tool meant to find it. This releases
them and lets apt's own retention rules decide, with the running and newest kernels
excluded — and asserted afterwards, because the cost of that filter being wrong is a
guest that does not come back from a reboot.
That second guard also records the trap that made this hard to measure:
dpkg-query -Wlistsrcpackages — removed, config files remaining — besideinstalled ones, each with a plausible
Installed-Size. Counting those is how aguest with two kernels reads as a guest with fourteen.
REBOOT PENDINGReported when the running kernel is not the newest installed.
autoremovekeepsboth, so the pile only collapses on the reboot that makes them the same package —
the difference between "this playbook found nothing" and "this playbook cannot
help until you reboot".
Verified end to end
cloudflared_vmwent from 100% used andUNREACHABLEto 77% used, 678 MiBfree, and a re-run is a clean no-op.
ansible-lintpasses at theproductionprofile;
yamllint,typosandlycheeclean.The README also notes when cleaning is the wrong answer — if the reclaim is small
and the guest fills again next week, the disk is too small and the fix belongs in
the OpenTofu unit.