From 342186c463bd84fdc7ac46778dcc7bae21527002 Mon Sep 17 00:00:00 2001 From: Harwinder Sekhon Date: Sat, 19 Sep 2026 19:31:59 -0400 Subject: [PATCH] Add comments and pending-update reporting to auto_updates.yml Documents each play/task, and adds per-host and final summary output showing how many packages are pending on each host. Co-Authored-By: Claude Sonnet 5 --- playbooks/auto_updates.yml | 94 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/playbooks/auto_updates.yml b/playbooks/auto_updates.yml index f57f4f0..e4ca8c1 100644 --- a/playbooks/auto_updates.yml +++ b/playbooks/auto_updates.yml @@ -3,35 +3,46 @@ # dnf-automatic on Rocky Linux. Neither reboots automatically; use # reboot_check.yml for that. +# Play 1: probe every ubuntu/rocky host and sort them into a "reachable_hosts" +# group so the two update plays below can skip anything that's offline. - name: Check Host Availability hosts: ubuntu:rocky gather_facts: false ignore_unreachable: true tasks: + # Ping each host; failures/unreachables are swallowed so the play keeps + # going instead of aborting the whole run. - name: Ping hosts to check availability ansible.builtin.ping: register: ping_result ignore_errors: true ignore_unreachable: true + # Dynamically creates/populates the "reachable_hosts" group, used by the + # Ubuntu and Rocky plays' host patterns (ubuntu:&reachable_hosts etc.). - name: Add reachable hosts to group ansible.builtin.group_by: key: reachable_hosts when: ping_result is reachable and ping_result is succeeded + # Print a message for any host that failed the ping, so offline hosts + # are visible in the run output. - name: Report offline hosts ansible.builtin.debug: msg: "{{ inventory_hostname }} - OFFLINE" when: ping_result is unreachable or ping_result is failed delegate_to: localhost +# Play 2: install and configure unattended-upgrades on Ubuntu hosts, then +# report its status. Does not enable automatic reboots. - name: Auto Updates - Ubuntu hosts: ubuntu:&reachable_hosts become: true gather_facts: false tasks: + # Make sure the unattended-upgrades package is present. - name: Install unattended-upgrades ansible.builtin.apt: name: unattended-upgrades @@ -39,6 +50,8 @@ update_cache: true cache_valid_time: 3600 + # Turn on apt's periodic package-list refresh, unattended-upgrade run, + # and weekly autoclean. - name: Enable periodic package list refresh and upgrades ansible.builtin.copy: dest: /etc/apt/apt.conf.d/20auto-upgrades @@ -51,6 +64,8 @@ group: root mode: '0644' + # Local overrides layered on top of the stock 50unattended-upgrades + # config: clean up unused deps, but never reboot automatically. - name: Configure unattended-upgrades behaviour ansible.builtin.copy: dest: /etc/apt/apt.conf.d/52unattended-upgrades-local @@ -63,6 +78,8 @@ group: root mode: '0644' + # Enable and start the systemd timers that actually trigger apt's + # periodic update/upgrade cycle. - name: Enable apt timers ansible.builtin.systemd: name: "{{ item }}" @@ -72,17 +89,32 @@ - apt-daily.timer - apt-daily-upgrade.timer + # Simulate an unattended-upgrade run (no changes made) so the report + # below shows what would actually be upgraded. - name: Dry-run unattended-upgrade ansible.builtin.command: unattended-upgrade --dry-run -v register: uu_dry_run changed_when: false failed_when: false + # Pull the package list out of the dry-run's "Packages that will be + # upgraded: ..." line, used for the per-host and final summary reports. + - name: Extract pending package list + ansible.builtin.set_fact: + pending_update_packages: "{{ (uu_dry_run.stdout | regex_findall('Packages that will be upgraded:\\s*(.*)') | first | default('')).split() }}" + + - name: Compute pending update count + ansible.builtin.set_fact: + pending_update_count: "{{ pending_update_packages | length }}" + + # Grab the current state of the apt-daily timers for the report. - name: List apt timers # noqa: command-instead-of-module (read-only listing) ansible.builtin.command: systemctl list-timers apt-daily* --no-pager --all register: apt_timers changed_when: false + # Summarize timer status, pending update count, and the tail of the + # dry-run output per host. - name: Print report ansible.builtin.debug: msg: | @@ -91,22 +123,30 @@ ====================================================== TIMERS: {{ apt_timers.stdout }} + ------------------------------------------------------ + PENDING UPDATES: {{ pending_update_count }} + {{ pending_update_packages | join(', ') }} ------------------------------------------------------ DRY RUN (last lines): {{ (uu_dry_run.stdout_lines + uu_dry_run.stderr_lines)[-6:] | join('\n') }} ====================================================== +# Play 3: install and configure dnf-automatic on Rocky Linux hosts (security +# updates only), then report its status. - name: Auto Updates - Rocky Linux hosts: rocky:&reachable_hosts become: true gather_facts: false tasks: + # Make sure the dnf-automatic package is present. - name: Install dnf-automatic ansible.builtin.dnf: name: dnf-automatic state: present + # Set dnf-automatic to download and apply security updates only, and + # log output to stdout (captured by the systemd journal). - name: Configure dnf-automatic for security updates community.general.ini_file: path: /etc/dnf/automatic.conf @@ -122,17 +162,38 @@ loop_control: label: "{{ item.section }}/{{ item.option }} = {{ item.value }}" + # Enable and start the systemd timer that triggers dnf-automatic runs. - name: Enable dnf-automatic timer ansible.builtin.systemd: name: dnf-automatic.timer enabled: true state: started + # Grab the current state of the dnf-automatic timer for the report. - name: List dnf-automatic timers # noqa: command-instead-of-module (read-only listing) ansible.builtin.command: systemctl list-timers dnf-automatic* --no-pager --all register: dnf_timers changed_when: false + # Check what security updates are pending. rc 100 means updates are + # available, rc 0 means none, so only other codes count as a failure. + - name: Check for pending security updates + ansible.builtin.command: dnf check-update --security + register: dnf_check + changed_when: false + failed_when: dnf_check.rc not in [0, 100] + + # Pull the "name.arch version repo" lines out of the check-update + # output, used for the per-host and final summary reports. + - name: Extract pending package list + ansible.builtin.set_fact: + pending_update_packages: "{{ dnf_check.stdout_lines | select('match', '^\\S+\\.\\S+\\s+\\S+\\s+\\S+$') | map('regex_replace', '^(\\S+)\\s.*$', '\\1') | list }}" + + - name: Compute pending update count + ansible.builtin.set_fact: + pending_update_count: "{{ pending_update_packages | length }}" + + # Summarize timer status and pending update count per host. - name: Print report ansible.builtin.debug: msg: | @@ -141,4 +202,37 @@ ====================================================== TIMERS: {{ dnf_timers.stdout }} + ------------------------------------------------------ + PENDING UPDATES: {{ pending_update_count }} + {{ pending_update_packages | join(', ') }} ====================================================== + +# Play 4: after both OS-specific plays have run, print one consolidated +# summary of which hosts have pending updates and how many packages. +- name: Summary - Pending Updates + hosts: localhost + gather_facts: false + + tasks: + - name: Print hosts with pending updates + ansible.builtin.debug: + msg: "{{ item }} - {{ hostvars[item].pending_update_count }} package(s) pending: {{ hostvars[item].pending_update_packages | join(', ') }}" + loop: "{{ groups['reachable_hosts'] | default([]) }}" + when: hostvars[item].pending_update_count | default(0) | int > 0 + + - name: Print hosts fully up to date + ansible.builtin.debug: + msg: "{{ item }} - up to date" + loop: "{{ groups['reachable_hosts'] | default([]) }}" + when: hostvars[item].pending_update_count is defined and hostvars[item].pending_update_count | int == 0 + + - name: Print totals + ansible.builtin.debug: + msg: >- + {{ groups['reachable_hosts'] | default([]) | length }} host(s) reachable, + {{ groups['reachable_hosts'] | default([]) + | map('extract', hostvars, 'pending_update_count') | select('defined') | map('int') | select('>', 0) | list | length }} + with pending updates, + {{ groups['reachable_hosts'] | default([]) + | map('extract', hostvars, 'pending_update_count') | select('defined') | map('int') | sum }} + package(s) total pending.