Skip to main content
RunBook Academy

← All break/fix scenarios in Ansible

advancedKernel~40 min

Break/Fix: one server never came back from a kernel patch and the pre-flight check said it had plenty of room

Reported symptoms

  • A kernel patch play across 40 hosts completed on 39 and timed out waiting for one to return
  • The failing host does not respond to SSH or ICMP and has not for twenty minutes
  • The play reported the package upgrade on that host as successful before the reboot
  • The pre-flight check that asserts free space on /boot passed on every host including this one
  • The 39 hosts that returned are running the new kernel and are healthy
  • Power-cycling the host does not help; it comes up and stops in the same place

Evidence

  • · The reboot task failed with a timeout waiting for the host to respond
  • · The upgrade task output includes a warning about running out of disk space during initramfs generation
  • · The pre-flight assertion read a fact rather than measuring the filesystem at run time
  • · `ansible-config dump | grep -i fact_caching` shows a cache configured with a long timeout
  • · The cached fact for that host was written three days earlier, before the previous kernel was installed
  • · The serial console shows the boot stopping at an initramfs prompt
  • · `df -h /boot` on a comparable host shows the filesystem close to full after two kernels accumulate
Diagnosis and resolutionclick to reveal

Root cause

The host is sitting at an initramfs prompt because the initial ramdisk for the new kernel was never generated completely. The `/boot` filesystem was almost full before the upgrade began, the package upgrade downloaded and unpacked the new kernel, and the post-installation step that builds the initramfs ran out of space and emitted a warning rather than an error. The package manager reported success, Ansible reported the task as changed and successful, and the host rebooted into a kernel whose ramdisk is truncated - so the root filesystem is never mounted and the boot stops, waiting for input at a prompt nobody is watching. The pre-flight guard that exists precisely to prevent this did not fail, because it asserted on a fact from the cache rather than on a measurement taken during the run: the cached value was written three days earlier, before the previous kernel installation consumed most of the remaining space, and it described a filesystem that had not existed for three days. A guard that reads stale data is worse than no guard, because it produces the confidence that the absent guard would not.

Remediation

Recovery requires console or out-of-band access, because a host stopped at an initramfs prompt has no network stack and no SSH. Boot the previous kernel from the boot loader menu to get a working system, then reclaim space on `/boot` by removing superseded kernels through the package manager rather than by deleting files, regenerate the initramfs for the new kernel, and confirm the boot loader entry is complete before rebooting again. Then fix the guard: it must measure the filesystem during the run rather than read a fact, and it must run immediately before the upgrade rather than at the start of the play. Fix the upgrade task in the same change so that a warning during initramfs generation is treated as a failure, and add a post-upgrade check that the new initramfs exists and is a plausible size before any reboot is attempted.

Verification

Prove the guard can fail: on a scratch host, fill `/boot` deliberately and confirm the pre-flight refuses to start the upgrade. This is the check that was missing, and a guard that has only ever passed has not been shown to work. Confirm the recovered host boots the new kernel unattended by rebooting it once more and watching it return without intervention. Confirm the initramfs for the running kernel exists and is comparable in size to the one for the previous kernel. Across the fleet, confirm free space on `/boot` measured now rather than from any cache, and confirm the running kernel version matches the installed one on every host.

Prevention

Guards must measure, not remember. Any assertion about a resource that can change - disk space, memory, service state, package version - has to be based on a value gathered during the run, and any play that relies on cached facts for a safety decision should refresh them first or gather explicitly. Place the guard immediately before the dangerous step, not at the top of the play, so nothing can consume the resource in between. Treat a reboot as an irreversible step that needs an explicit precondition: the new kernel installed, its initramfs generated and verified, the boot loader updated, and free space still adequate. Keep `/boot` sized for at least three kernels and prune superseded ones as part of the patch cycle rather than as a separate chore. And ensure out-of-band access exists and is tested before any fleet reboot, because one host in forty not returning is the expected case rather than the surprising one.

Reported symptoms

The monthly kernel patch runs across 40 application hosts at 01:00. It completes on 39. The fortieth times out.

TASK [Reboot into the new kernel] **********************************************
fatal: [app017]: FAILED! => {"changed": false, "msg": "Timed out waiting for last boot time check (timeout=600)"}

Twenty minutes later app017 still does not answer SSH or ICMP. The on-call engineer power-cycles it through the management processor. It comes up and stops in the same place.

What makes this confusing rather than routine:

  • The play has a pre-flight check that asserts free space on /boot. It passed on this host.
  • The package upgrade task on this host reported success.
  • The other 39 hosts are running the new kernel and are perfectly healthy.

So the first theory is hardware, and twenty minutes go into the management processor logs, which are clean.

Evidence provided

Read-only / Safethe guard passed and the upgrade succeeded
$ grep -B2 -A6 'app017' logs/patch-0100.log | head -20
TASK [Assert enough free space on /boot] ***************************************
ok: [app017]

TASK [Install the new kernel] **************************************************
changed: [app017]

TASK [Reboot into the new kernel] **********************************************
fatal: [app017]: FAILED! => {"changed": false, "msg": "Timed out waiting for last boot time check (timeout=600)"}
Read-only / Safethe upgrade said so, in output nobody reads on a green task
$ grep -A4 'Install the new kernel' logs/patch-0100-verbose.log | grep -i 'warning\|space'
update-initramfs: Generating /boot/initrd.img-6.8.0-52-generic
zstd: error 70 : Write error : cannot write block : No space left on device
E: mkinitramfs failure zstd 70
update-initramfs: failed for /boot/initrd.img-6.8.0-52-generic with 1.
Read-only / Safethe guard reads a fact
$ grep -n -A6 'Assert enough free space' patch.yml
8:- name: Assert enough free space on /boot
9:  ansible.builtin.assert:
10:    that:
11:      - (ansible_mounts | selectattr('mount','equalto','/boot') | first).size_available > 200000000
12:    fail_msg: "Not enough free space on /boot to install a kernel"
Read-only / Safefacts are cached for a week
$ ansible-config dump | grep -i 'fact_caching'
CACHE_PLUGIN(/srv/automation/ansible.cfg) = jsonfile
CACHE_PLUGIN_CONNECTION(/srv/automation/ansible.cfg) = /var/cache/ansible/facts
CACHE_PLUGIN_TIMEOUT(/srv/automation/ansible.cfg) = 604800
Read-only / Safewritten three days ago, describing 400 MB of free space
$ stat -c '%y' /var/cache/ansible/facts/app017; jq '.ansible_mounts[] | select(.mount=="/boot") | .size_available' /var/cache/ansible/facts/app017
2026-08-08 03:11:44.000 +0000
418906112
Read-only / Safea comparable host, measured now - the cached figure was never true this week
$ ansible app018 -i inventory -b -m ansible.builtin.command -a 'df -h /boot'
Filesystem      Size  Used Avail Use% Mounted on
/dev/sda2       512M  474M   11M  98% /boot

Work the evidence before reading on

The guard passed and the host is unbootable. Both of those are facts, so the guard was answering a question about something other than the current state of the host.

  1. Compare the free-space figure the guard used against the figure a comparable host reports today. What is the difference, and what happened in between?
  2. The upgrade task reported changed and success. Read the verbose output of that same task. Do the two agree?
  3. The other 39 hosts returned. Does that tell you the guard worked on them, or only that they had more room?

Before continuing: when was the value the guard asserted on actually measured, and by what?

Root cause

1. The guard asserted on a three-day-old measurement

The assertion reads ansible_mounts, which is a fact. Facts on this controller are cached, with a timeout of a week.

The cached entry for app017 was written three days earlier. At that time /boot had roughly 400 MB free. Since then the previous month’s kernel had been installed and had consumed most of it, so the real figure on the night of the run was around 11 MB.

The guard compared 400 MB against its threshold, passed, and reported ok. Everything about that is correct behaviour: the fact was in the cache, the cache was inside its timeout, and the assertion evaluated a true statement about a filesystem that had not existed for three days.

A cached fact is a claim about a host in the past. Using one for a safety decision converts the guard into a record of history.

2. The upgrade succeeded and the initramfs did not

The package manager installed the kernel package. Its post-installation step, which builds the initial ramdisk, ran out of space and failed.

Whether that failure propagates to a non-zero exit status depends on the distribution and the specific trigger, and in this case it did not - the package was configured, the upgrade reported success, and the module reported changed. The failure text was in the module output, which nobody reads on a task that reports success.

The host was left with a boot loader entry pointing at a new kernel and a truncated ramdisk.

3. The reboot task did its job and could not have helped

ansible.builtin.reboot issues the reboot, waits for the host to respond, and runs a test command to confirm it is usable. The default timeout is 600 seconds and the default test command is whoami.

None of that can detect a host that will never come back. The module waited the full timeout and reported it accurately. The failure message is correct and uninformative, because from the controller’s point of view a host that stops in its initramfs and a host whose power supply failed look identical.

Resolution

  1. Connect to the console or management processor. There is no network path to a host stopped in its initramfs, and every step below needs one.
  2. Confirm the diagnosis from the console. An initramfs prompt with a message about being unable to find the root device is this fault; a kernel panic or a failed hardware check is a different incident with a different fix.
  3. Boot the previous kernel from the boot loader menu to get a working system. Do not try to repair anything from the initramfs prompt if a working kernel is available.
  4. Reclaim space on /boot by removing superseded kernels through the package manager, so the boot loader configuration is regenerated correctly. Deleting files by hand leaves the boot loader referring to entries that no longer exist.
  5. Regenerate the initramfs for the new kernel and confirm it completes without warnings and produces a file comparable in size to the one for the previous kernel.
  6. Reboot once more and confirm the host returns unattended into the new kernel. Watching it come back is the only proof that matters.
  7. Fix the guard: measure free space during the run rather than reading a fact, and place the assertion immediately before the upgrade rather than at the top of the play.
  8. Fix the upgrade step so a failed initramfs generation fails the task, and add a post-upgrade check that the initramfs exists and is plausible before any reboot is attempted.

Verification

  1. The guard can fail. On a scratch host, fill /boot deliberately and confirm the pre-flight refuses to start the upgrade. This is the check that would have prevented the incident and it had never been exercised.
  2. The recovered host reboots unattended. Reboot it once more and confirm it returns without console intervention; a host that needed a person to come back is not recovered.
  3. The running kernel matches the installed kernel. Compare the running version against the newest installed version on every host in the fleet, not just the repaired one. This catches the silent variant where a host reboots successfully into the old kernel.
  4. The initramfs is intact. The file for the running kernel exists and is comparable in size to the one for the previous kernel; a truncated ramdisk is often the only visible difference.
  5. Free space is adequate, measured now. Run a df across the fleet in this run rather than reading any cached figure, and require headroom for at least one more kernel.
  6. A failed initramfs generation fails the task. Reproduce it on a scratch host and confirm the play stops before the reboot rather than after it.
  7. Out-of-band access works on every host that will be rebooted. Test it before the next fleet patch, not during it.

Prevention

  • Guards measure, they do not remember. Any assertion about a resource that can change needs a value gathered in this run:
- name: Measure free space on /boot now
  ansible.builtin.command: df --output=avail --block-size=1 /boot
  changed_when: false
  register: boot_free

- name: Refuse to install a kernel without room for its initramfs
  ansible.builtin.assert:
    that: boot_free.stdout_lines[1] | int > 300000000
    fail_msg: "Only {{ boot_free.stdout_lines[1] | int }} bytes free on /boot; refusing to install a kernel."
  • Put the guard immediately before the dangerous step. A check at the top of a play is a check about a state that several tasks have had a chance to change.
  • Refresh or bypass the fact cache for plays that make safety decisions. Caching is worth having; caching a value a guard depends on is not.
  • Treat a reboot as irreversible and give it an explicit precondition: the kernel installed, the initramfs generated and verified, the boot loader updated, and free space still adequate.
  • Verify the running kernel after every patch reboot. It is the check that catches both the host that never returns and the host that returns into the wrong kernel.
  • Size /boot for at least three kernels and prune superseded ones as part of the patch cycle. Almost every incident of this shape is a 512 MB /boot that was adequate two kernel sizes ago.
  • Ensure out-of-band access exists and is tested before any fleet reboot. One host in forty not returning is the expected case, not the surprising one.