diff --git a/life-church/playbooks/thinkcenter-firmware.yml b/life-church/playbooks/thinkcenter-firmware.yml new file mode 100644 index 0000000..2d23503 --- /dev/null +++ b/life-church/playbooks/thinkcenter-firmware.yml @@ -0,0 +1,167 @@ +--- +# ============================================================================ +# life-church/playbooks/thinkcenter-firmware.yml +# Firmware (LVFS/fwupd) upgrade + controlled reboot for thinkcenter ONLY. +# HIGH-RISK: applies device/UEFI firmware and REBOOTS the machine. +# become password from Infisical /lc-hosts at runtime. +# +# Behaviour: +# - Installs fwupd if missing; refreshes LVFS metadata. +# - Lists available firmware updates (get-updates). +# - Applies updates with `fwupdmgr update` ONLY if updates exist. +# Many UEFI/system-firmware updates are STAGED and flashed during the next +# reboot (fwupd schedules them), so a reboot is required to complete them. +# - Reboots the host (guarded by `do_reboot`, default true) and waits for it +# to come back, then reports post-reboot firmware state. +# +# Controls (extra-vars / survey): +# do_reboot=true|false -> set false to apply/stage firmware but NOT reboot. +# force_reboot=true -> reboot even if no firmware update was applied +# (e.g. to also pick up a pending kernel update). +# +# UNDO: firmware is generally NOT downgradable safely. fwupd keeps history: +# fwupdmgr get-history # what was applied +# Downgrade (only if a vendor build allows): fwupdmgr downgrade +# This is a ONE-WAY door for most UEFI updates — treat as irreversible. +# ============================================================================ + +# --- Play 1 (localhost): fetch thinkcenter sudo pass from Infisical ---------- +- name: "thinkcenter firmware — resolve become password from Infisical" + hosts: localhost + gather_facts: false + tasks: + - name: "[resolve] Infisical creds from env" + ansible.builtin.set_fact: + infisical_client_id: "{{ lookup('env','INFISICAL_CLIENT_ID') }}" + infisical_client_secret: "{{ lookup('env','INFISICAL_CLIENT_SECRET') }}" + infisical_project_id: "{{ lookup('env','INFISICAL_PROJECT_ID') }}" + infisical_url: "{{ lookup('env','INFISICAL_API_URL') | default('http://192.168.122.41:8080', true) }}" + infisical_env_slug: "{{ lookup('env','INFISICAL_ENV_SLUG') | default('prod', true) }}" + + - name: "[resolve] Assert Infisical creds present" + ansible.builtin.assert: + that: + - infisical_client_id | length > 0 + - infisical_client_secret | length > 0 + - infisical_project_id | length > 0 + fail_msg: "Infisical creds missing from Semaphore environment 'infisical-dev'." + + - name: "[infisical] Log in" + infisical.vault.login: + url: "{{ infisical_url }}" + auth_method: universal_auth + universal_auth_client_id: "{{ infisical_client_id }}" + universal_auth_client_secret: "{{ infisical_client_secret }}" + register: infisical_login + no_log: true + + - name: "[infisical] Read /lc-hosts" + infisical.vault.read_secrets: + login_data: "{{ infisical_login.login_data }}" + project_id: "{{ infisical_project_id }}" + env_slug: "{{ infisical_env_slug }}" + path: "/lc-hosts" + as_dict: true + register: lc_secrets + no_log: true + + - name: "[set] Stash thinkcenter sudo pass" + ansible.builtin.set_fact: + thinkcenter_become_pass: "{{ lc_secrets.secrets['THINKCENTER_SUDO_PASS'] }}" + no_log: true + +# --- Play 2: firmware upgrade + reboot on thinkcenter ONLY ------------------- +- name: "Firmware upgrade + reboot thinkcenter" + hosts: thinkcenter + gather_facts: true + become: true + vars: + ansible_become_pass: "{{ hostvars['localhost'].thinkcenter_become_pass }}" + do_reboot: true # override to false to stage firmware without rebooting + force_reboot: false # reboot even if no firmware was applied + tasks: + - name: "[safety] Confirm we are on thinkcenter" + ansible.builtin.assert: + that: inventory_hostname == 'thinkcenter' + fail_msg: "Refusing to run — this play is scoped to thinkcenter only." + + - name: "[fwupd] Ensure fwupd is installed" + ansible.builtin.apt: + name: fwupd + state: present + update_cache: true + cache_valid_time: 3600 + + - name: "[fwupd] Refresh LVFS metadata (force)" + ansible.builtin.command: fwupdmgr refresh --force + register: fw_refresh + changed_when: false + failed_when: fw_refresh.rc not in [0, 2] # rc 2 = metadata already current + + - name: "[fwupd] List available firmware updates" + ansible.builtin.command: fwupdmgr get-updates --json + register: fw_updates_json + changed_when: false + failed_when: false + + - name: "[fwupd] Parse whether any updates are available" + ansible.builtin.set_fact: + fw_update_devices: >- + {{ (fw_updates_json.stdout | default('{}') | from_json).Devices | default([]) + | selectattr('Releases','defined') | map(attribute='Name') | list }} + + - name: "[fwupd] Set has-updates flag (devices carrying a Releases payload)" + ansible.builtin.set_fact: + fw_has_updates: "{{ fw_update_devices | length > 0 }}" + + - name: "[fwupd] Show updatable devices (if any)" + ansible.builtin.debug: + msg: "{{ fw_update_devices if fw_has_updates else 'No firmware updates available.' }}" + + - name: "[fwupd] Apply firmware updates (assume-yes, no interactive reboot)" + ansible.builtin.command: fwupdmgr update --assume-yes --no-reboot-check + register: fw_apply + when: fw_has_updates + changed_when: fw_apply.rc == 0 + failed_when: fw_apply.rc not in [0, 2] # rc 2 = nothing to do + + - name: "[reboot] Check for pending OS reboot flag too" + ansible.builtin.stat: + path: /var/run/reboot-required + register: os_reboot_flag + + - name: "[reboot] Decide whether to reboot" + ansible.builtin.set_fact: + reboot_now: "{{ do_reboot | bool and (fw_has_updates | bool or force_reboot | bool or os_reboot_flag.stat.exists) }}" + + - name: "[reboot] Reboot to flash staged firmware / complete updates" + ansible.builtin.reboot: + msg: "Rebooting to apply firmware/system updates (Ansible)" + reboot_timeout: 600 # wait up to 10 min for it to come back + pre_reboot_delay: 5 + post_reboot_delay: 30 + test_command: fwupdmgr --version + when: reboot_now | bool + + - name: "[verify] fwupd history after run" + ansible.builtin.command: fwupdmgr get-history + register: fw_history + changed_when: false + failed_when: false + + - name: "[verify] Uptime after (confirms reboot happened)" + ansible.builtin.command: uptime -p + register: post_uptime + changed_when: false + + - name: "[report] Firmware run summary" + ansible.builtin.debug: + msg: + - "===== thinkcenter firmware =====" + - "Updates available this run : {{ fw_has_updates }}" + - "Firmware applied : {{ fw_apply.changed | default(false) }}" + - "OS reboot flag was set : {{ os_reboot_flag.stat.exists }}" + - "Rebooted this run : {{ reboot_now }}" + - "Uptime now : {{ post_uptime.stdout }}" + - "fwupd history (last applied): see get-history output on host" + - "NOTE: some UEFI updates flash during POST on the reboot — verify BIOS version in-firmware if critical."