--- - name: Gather package facts before patching ansible.builtin.package_facts: manager: auto register: packages_before - name: Store pre-patch package versions ansible.builtin.set_fact: packages_pre_patch: "{{ ansible_facts.packages }}" - name: Check for available updates (Debian/Ubuntu) ansible.builtin.apt: update_cache: true cache_valid_time: 3600 when: ansible_os_family == "Debian" changed_when: false - name: Get list of upgradable packages (Debian/Ubuntu) ansible.builtin.shell: | apt list --upgradable 2>/dev/null | grep -v "Listing..." | awk -F'/' '{print $1}' register: upgradable_packages changed_when: false when: ansible_os_family == "Debian" - name: Get list of upgradable packages (RHEL/CentOS) ansible.builtin.shell: | dnf check-update --quiet | awk '{print $1}' | grep -v "^$" register: upgradable_packages changed_when: false failed_when: upgradable_packages.rc not in [0, 100] when: ansible_os_family == "RedHat" - name: Get list of upgradable packages (Alpine) ansible.builtin.shell: | apk list --upgradable 2>/dev/null | awk -F'-[0-9]' '{print $1}' register: upgradable_packages changed_when: false when: ansible_os_family == "Alpine" - name: Log packages to be updated ansible.builtin.debug: msg: "Packages to be updated on {{ inventory_hostname }}: {{ upgradable_packages.stdout_lines | length }} packages" - name: Perform full upgrade (Debian/Ubuntu) ansible.builtin.apt: upgrade: dist autoremove: true autoclean: true register: apt_upgrade_result when: - ansible_os_family == "Debian" - patch_mode == "full" or patch_mode == "security" - name: Perform security-only upgrade (RHEL/CentOS) ansible.builtin.dnf: name: "*" state: latest security: "{{ patch_mode == 'security' }}" update_cache: true register: dnf_upgrade_result when: ansible_os_family == "RedHat" - name: Perform upgrade (Alpine) ansible.builtin.shell: | apk update && apk upgrade register: apk_upgrade_result changed_when: "'OK' in apk_upgrade_result.stdout" when: ansible_os_family == "Alpine" - name: Gather package facts after patching ansible.builtin.package_facts: manager: auto - name: Store post-patch package versions ansible.builtin.set_fact: packages_post_patch: "{{ ansible_facts.packages }}" - name: Calculate changed packages ansible.builtin.set_fact: packages_updated: >- {{ packages_post_patch | dict2items | selectattr('key', 'in', packages_pre_patch) | selectattr('value', '!=', packages_pre_patch[item.key] | default([])) | list | map(attribute='key') | list }} loop: "{{ packages_post_patch | dict2items }}" when: false - name: Build packages updated list ansible.builtin.set_fact: packages_updated: >- {%- set updated = [] -%} {%- for pkg, details in packages_post_patch.items() -%} {%- if pkg in packages_pre_patch -%} {%- if details[0].version != packages_pre_patch[pkg][0].version -%} {%- set _ = updated.append({ 'name': pkg, 'version_before': packages_pre_patch[pkg][0].version, 'version_after': details[0].version, 'type': 'updated' }) -%} {%- endif -%} {%- else -%} {%- set _ = updated.append({ 'name': pkg, 'version_before': 'not installed', 'version_after': details[0].version, 'type': 'new' }) -%} {%- endif -%} {%- endfor -%} {{ updated }} - name: Log updated packages ansible.builtin.debug: msg: "Updated: {{ item.name }} {{ item.version_before }} -> {{ item.version_after }}" loop: "{{ packages_updated }}" - name: Check if reboot is required after patching (Debian/Ubuntu) ansible.builtin.stat: path: /var/run/reboot-required register: reboot_required_post when: ansible_os_family == "Debian" - name: Update reboot required fact ansible.builtin.set_fact: host_reboot_required: "{{ reboot_required_post.stat.exists | default(false) }}" when: ansible_os_family == "Debian" - name: Check if reboot is required after patching (Alpine) ansible.builtin.shell: | apk version -l = 2>/dev/null | grep -q kernel && echo "yes" || echo "no" register: alpine_reboot_check changed_when: false when: ansible_os_family == "Alpine" - name: Update reboot required fact (Alpine) ansible.builtin.set_fact: host_reboot_required: "{{ alpine_reboot_check.stdout | trim == 'yes' }}" when: ansible_os_family == "Alpine" - name: Reboot if required and auto_reboot is enabled ansible.builtin.reboot: reboot_timeout: 300 pre_reboot_delay: 10 post_reboot_delay: 30 msg: "Rebooting after patch run — initiated by Ansible" when: - host_reboot_required | bool - auto_reboot | bool - name: Patching complete ansible.builtin.debug: msg: "Patching complete on {{ inventory_hostname }} — {{ packages_updated | length }} packages updated"