Skip to main content
RunBook Academy

AnsibleXL · Patch and Reboot ManagementPatch and Reboot Management

Deciding whether a reboot is required

Advanced⏱ ~25 minansible-playbook

What you'll learn

  • Detect reboot requirement on Debian-family and RHEL-family hosts using the mechanism each family actually provides
  • Handle the inverted exit status of dnf needs-restarting -r without failing the task
  • Compare the running kernel against installed kernels using facts rather than shell
  • Record the reboot decision so the run log explains why each host was or was not rebooted

Prerequisites

Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11

Not yet marked complete on this device.

Rebooting every host you patched is wasteful. Rebooting no host you patched is dangerous. The interesting work is in between, and it starts with a question the host can answer for itself: do you need a reboot?

The two distribution families answer it by entirely different mechanisms, and neither one is a superset of the other. Writing a play that treats one as universal produces a fleet where half the hosts are never rebooted and the run log says everything is fine.

Debian and Ubuntu: a file, if something wrote it

On Debian-family hosts, a package that needs a reboot creates /var/run/reboot-required. A companion file, /var/run/reboot-required.pkgs, lists the packages that asked for it, one per line.

Both live under /run on modern systems — /var/run is a symlink — so they are tmpfs files that vanish on boot. That is the property that makes them correct: the flag cannot survive the reboot it was asking for.

Read-only / Safeasking a Debian-family host whether it needs a reboot
- name: Check for the Debian-family reboot flag
ansible.builtin.stat:
  path: /var/run/reboot-required
  get_checksum: false
register: reboot_flag
when: ansible_facts.os_family == 'Debian'

- name: Read which packages requested the reboot
ansible.builtin.slurp:
  src: /var/run/reboot-required.pkgs
register: reboot_pkgs_raw
when:
  - ansible_facts.os_family == 'Debian'
  - reboot_flag.stat.exists | default(false)
failed_when: false

- name: Record the reason
ansible.builtin.set_fact:
  reboot_required: '{{ reboot_flag.stat.exists | default(false) }}'
  reboot_reason: >-
    {{ (reboot_pkgs_raw.content | default('') | b64decode).split('\n')
       | reject('equalto', '') | list }}
when: ansible_facts.os_family == 'Debian'

get_checksum: false is worth setting. stat defaults it to true, and checksumming a file you only want the existence of is pure cost — trivial here, and not trivial when the same habit is applied to a large file later.

Read-only / Safeproving the detection mechanism is present before trusting it
- name: Confirm the reboot-required mechanism is installed
ansible.builtin.stat:
  path: /usr/share/update-notifier/notify-reboot-required
  get_checksum: false
register: notify_helper
when: ansible_facts.os_family == 'Debian'

- name: Refuse to trust reboot detection on a host that cannot signal it
ansible.builtin.fail:
  msg: >-
    {{ inventory_hostname }} has no notify-reboot-required helper, so
    /var/run/reboot-required will never appear regardless of what is
    patched. Install update-notifier-common or exclude this host from
    flag-based detection.
when:
  - ansible_facts.os_family == 'Debian'
  - not (notify_helper.stat.exists | default(false))

Debian-family hosts may additionally have needrestart, which answers a different and complementary question: which running daemons are using libraries that have been replaced. Its -b option enables batch mode, documented as “don’t restart anything and produce machine-readable output”, and -r l sets list-only restart mode. That tells you about service restarts, not about kernel reboots, and the two decisions are genuinely separate.

RHEL, Rocky and Alma: an exit status, and it is inverted

The RHEL-family mechanism is needs-restarting, from dnf-plugins-core. With -r it answers only the reboot question, and it answers by exit code.

The documentation is explicit, and the convention is backwards from everything else in a playbook:

Only report whether a reboot is required (exit code 1) or not (exit code 0).

Exit 1 means a reboot is needed. Exit 0 means it is not. To Ansible, a non-zero return code is a task failure, so a naive task fails on precisely the hosts you were trying to identify.

Read-only / Safehandling the inverted exit status
- name: Ask whether a reboot is required
ansible.builtin.command:
  argv: [dnf, needs-restarting, '-r']
register: needs_reboot
changed_when: false
failed_when: false
when: ansible_facts.os_family == 'RedHat'

- name: Record the decision
ansible.builtin.set_fact:
  reboot_required: '{{ needs_reboot.rc == 1 }}'
when: ansible_facts.os_family == 'RedHat'

Three deliberate choices in that task.

changed_when: false because asking a question is not a change. A command task reports changed by default, and an inspection task that colours the run yellow trains people to ignore yellow.

failed_when: false because the exit code is data, not a verdict. Without it, exit 1 fails the host and — under max_fail_percentage: 0 — stops the play, on the hosts that most needed patching.

argv: rather than a command string because the argument list is explicit and nothing goes near a shell. The command module does not invoke a shell anyway, and argv makes that visible.

needs-restarting -s (or --services) lists only the affected systemd services, which is the RHEL-family counterpart of what needrestart does on Debian: the service-restart question, distinct from the reboot question.

The comparison that works on both families

The running kernel and the installed kernel are two different things, and comparing them requires no distribution-specific tooling at all.

ansible_facts.kernel is the running kernel — verified against ansible-core 2.21.3, where a setup run reported ansible_kernel: 7.0.0-29-generic, the same string uname -r prints. It is gathered at the start of the play, which means it is the kernel that was running when facts were gathered, and after a reboot you must gather again to see the new one.

ansible.builtin.package_facts gives you what is installed. Its structure is the thing to get right: ansible_facts.packages is a dictionary keyed by package name, and each value is a list, because a host can legitimately have several versions of the same package — which is exactly the situation kernels are always in.

Read-only / Saferunning kernel versus newest installed kernel
- name: Collect the installed package inventory
ansible.builtin.package_facts:
  manager: auto

- name: Work out the newest installed kernel
ansible.builtin.set_fact:
  kernel_package: >-
    {{ 'linux-image-generic' if ansible_facts.os_family == 'Debian'
       else 'kernel' }}

- name: Compare the running kernel against what is installed
ansible.builtin.debug:
  msg: >-
    running={{ ansible_facts.kernel }}
    installed={{ ansible_facts.packages[kernel_package]
                 | default([]) | map(attribute='version') | list }}

For the specific question “is the running kernel the newest installed one”, ansible_facts.kernel compared against the sorted installed versions answers it directly, and it answers on a minimal host with no update-notifier-common and no dnf-plugins-core. That independence is what makes it the backstop.

Recording the decision

A reboot decision that exists only as a when: condition is invisible afterwards. Six weeks later, asked why db04 was not rebooted in the March window, nobody can answer.

Read-only / Safemaking the decision legible in the run log
- name: State the reboot decision and its basis
ansible.builtin.debug:
  msg: >-
    {{ inventory_hostname }}:
    reboot_required={{ reboot_required | default('unknown') }}
    detection={{ reboot_detection_worked | default(false) | ternary('ok', 'FAILED') }}
    running_kernel={{ ansible_facts.kernel }}
    reason={{ reboot_reason | default([]) | join(', ') | default('none', true) }}

The three states that matter are true, false and unknown. Most implementations have only the first two, and they encode “the detection did not work” as false, which is the same value as “the host is fine”.

Knowledge check

Knowledge check · 4 questions

  1. Q1. A task runs `dnf needs-restarting -r` with the command module and no failed_when. On which hosts does the task fail?

  2. Q2. A patch play stats /var/run/reboot-required on every host in a mixed Debian and Rocky fleet and reboots where it exists. What is the most serious defect?

  3. Q3. Why is comparing ansible_facts.kernel against the installed kernel packages a useful backstop alongside the per-family detection mechanisms? Select all that apply.

  4. Q4. If /var/run/reboot-required is absent on a Debian host after a kernel upgrade, the host definitely does not need a reboot.

Passing score: 75%. Answers are checked in this browser.