Ansible playbook to restart if needed

I’m trying to write a playbook that installs updates and then reboots a linux server if needed. For now I’m just working on dnf based systems. The dnf update works, but the reboot doesn’t. So far, I have come up with this:

file name: dnf-update.yml

---
- name: DNF Update/Upgrade
  hosts: "{{ target }}"
  become: true
  gather_facts: true
  tasks:
    - name: Check and install updates
      ansible.builtin.shell:
        cmd: "dnf -y update"
      register: dnf_results
      when: ansible_os_family == "RedHat"
    - name: Print Results
      debug:
        msg: "{{ dnf_results.stdout_lines }}"
      when: ansible_os_family == "RedHat"
    - name: install yum-utils if needed
      ansible.builtin.dnf:
        name: yum-utils
        state: present
      when: ansible_os_family == "RedHat"
    - name: check if reboot is required
      ansible.builtin.shell:
        cmd: "needs-restarting -r -s"
      register: needs_restarting
      changed_when: needs_restarting.rc == 1
    - name: do reboot if needed
      ansible.builtin.reboot:
      when: needs_restarting.rc == 1

I run it like this (only because I don’t know of a better way to do this while targeting only one host… if there is… let me know!)

ansible-playbook ./dnf-update -e "target=host.domain"

And I end up with a result that shows the following output:

TASK [check if reboot is required] **************************************************************
fatal: [host.domain]: FAILED! => {"changed": true, "cmd": "needs-restarting -r -s", "delta": "0:00:00.797461", "end": "2024-05-21 13:40:04.626996", "msg": "non-zero return code", "rc": 1, "start": "2024-05-21 13:40:03.829535", "stderr": "", "stderr_lines": [], "stdout": "Core libraries or services have been updated since boot-up:\n  * dbus\n  * dbus-daemon\n  * glibc\n  * kernel\n  * linux-firmware\n  * systemd\n  * zlib (dependency of dbus. Recommending reboot of dbus)\n\nReboot is required to fully utilize these updates.\nMore information: https://access.redhat.com/solutions/27943", "stdout_lines": ["Core libraries or services have been updated since boot-up:", "  * dbus", "  * dbus-daemon", "  * glibc", "  * kernel", "  * linux-firmware", "  * systemd", "  * zlib (dependency of dbus. Recommending reboot of dbus)", "", "Reboot is required to fully utilize these updates.", "More information: https://access.redhat.com/solutions/27943"]}

PLAY RECAP **************************************************************************************
host.domain           : ok=4    changed=1    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0 

I think this means the playbook never ran the final task, “name: do reboot if needed” and I’m not sure why (I don’t know if playbook tasks are done in order and they stop when it reaches the first task that fails, but that seems to be the case).

Mostly, I just want a working solution. If anyone has a working playbook to share that does this (install updates and reboot if needed), please share! If not, let me know if there is something wrong with this playbook and how to fix it.

I’m working on Rocky Linux 8 with ansible 6.3.0-1.

Let me know if I can provide any other info to help debug what is happening.

In addition to your changed_when you need a failed_when to prevent the non zero exit code from causing the task to fail.

4 Likes

Thank you! I thought it wouldn’t need that because needs-restarting returns either 0 or 1. since 1 means do restart, i thought setting that as the changed_when value would make it continue processing the next task (i.e. 1 is no longer a fail).

Is this a bug in how ansible processes changed_when ? If the return code is matched by changed_when, shouldn’t that count as not a failure?

For reference, adding this failed_when right below the changed_when, does make it work:

failed_when: needs_restarting.rc == 0

Here’s the full playbook, with “failed_when” added

---
- name: DNF Update/Upgrade
  hosts: "{{ target }}"
  become: true
  gather_facts: true
  tasks:
    - name: Check and install updates
      ansible.builtin.shell:
        cmd: "dnf -y update"
      register: dnf_results
      when: ansible_os_family == "RedHat"
    - name: Print Results
      debug:
        msg: "{{ dnf_results.stdout_lines }}"
      when: ansible_os_family == "RedHat"
    - name: install yum-utils if needed
      ansible.builtin.dnf:
        name: yum-utils
        state: present
      when: ansible_os_family == "RedHat"
    - name: check if reboot is required
      ansible.builtin.shell:
        cmd: "needs-restarting -r -s"
      register: needs_restarting
      changed_when: needs_restarting.rc == 1
      failed_when: needs_restarting.rc == 0
    - name: do reboot if needed
      ansible.builtin.reboot:
      when: needs_restarting.rc == 1

Hi,

failed_when: never failed_when: false (thanks @Denney-tech) would fit better IMO, as it would never fail (obviously) and you’d probably like to have pipelines not reporting unneeded errors.

That being said, you should probably use handlers here, that’s what they are made for.

2 Likes

I always use changed_when: false or failed_when: false for shell commands that should never change/fail a task.

never appears to be undefined, not a falsey statement, and quoting it to make it a string turns it into a truthy statement.

ansible localhost -m debug -a 'msg={{ never is truthy }}'
localhost | FAILED! => {
    "msg": "The task includes an option with an undefined variable. The error was: 'never' is undefined. 'never' is undefined. 'never' is undefined. 'never' is undefined"
}

ansible localhost -m debug -a 'msg={{ "never" is truthy }}'
localhost | SUCCESS => {
    "msg": true
}

2 Likes

Ah yes, silly mistake on my part. Fixed it in my comment; thanks !

2 Likes
---
- name: DNF Update/Upgrade
  hosts: "{{ target }}"
  become: true
  gather_facts: true
  tasks:
    - name: Check and install updates
      ansible.builtin.shell:
        cmd: "dnf -y update"
      register: dnf_results
      when: ansible_os_family == "RedHat"

    - name: Print Results
      debug:
        msg: "{{ dnf_results.stdout_lines }}"
      when: ansible_os_family == "RedHat"

    - name: install yum-utils if needed
      ansible.builtin.dnf:
        name: yum-utils
        state: present
      when: ansible_os_family == "RedHat"

    - name: check if reboot is required
      ansible.builtin.shell:
        cmd: "needs-restarting -r -s"
      register: needs_restarting
      changed_when: needs_restarting.rc == 1
      failed_when: false
      notify: do reboot if needed
  
  handlers:
    - name: do reboot if needed
      ansible.builtin.reboot:
      

Expanding on the handlers notion.

Also, it might not hurt to put all the when: ansible_os_family == “RedHat” into a block, so you only have to check it once.

---
- name: DNF Update/Upgrade
  hosts: "{{ target }}"
  become: true
  gather_facts: true
  tasks:
    - name: RedHat execution block
      when: ansible_os_family == "RedHat"
      block:
        - name: Check and install updates
          ansible.builtin.shell:
            cmd: "dnf -y update"
          register: dnf_results
    
        - name: Print Results
          debug:
            msg: "{{ dnf_results.stdout_lines }}"
    
        - name: install yum-utils if needed
          ansible.builtin.dnf:
            name: yum-utils
            state: present
    
        - name: check if reboot is required
          ansible.builtin.shell:
            cmd: "needs-restarting -r -s"
          register: needs_restarting
          changed_when: needs_restarting.rc == 1
          failed_when: false
          notify: do reboot if needed
  
  handlers:
    - name: do reboot if needed
      ansible.builtin.reboot:
2 Likes