From f35bd094c7f215661561d92fa3ed9522161d896d Mon Sep 17 00:00:00 2001 From: Vezpi Date: Wed, 17 Jun 2026 09:13:55 +0000 Subject: [PATCH 01/74] add: OPNsense HA cluster update playbook --- .../opnsense/update_opnsense_ha_cluster.yml | 495 ++++++++++++++++++ 1 file changed, 495 insertions(+) create mode 100644 ansible/opnsense/update_opnsense_ha_cluster.yml diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml new file mode 100644 index 0000000..7482be4 --- /dev/null +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -0,0 +1,495 @@ +--- +# OPNsense HA Cluster Update +# Order: backup (TrueNAS) first, then master (Proxmox) +############################################################### +- name: "Phase 1 | Firmware check on all nodes" + hosts: opnsense + gather_facts: false + vars: + opnsense_api_key: "{{ lookup('env', 'OPNSENSE_API_KEY') }}" + opnsense_api_secret: "{{ lookup('env', 'OPNSENSE_API_SECRET') }}" + + tasks: + - name: Check node availability + ansible.builtin.uri: + url: "https://{{ ansible_host }}/api/core/system/status" + method: GET + user: "{{ opnsense_api_key }}" + password: "{{ opnsense_api_secret }}" + force_basic_auth: true + # validate_certs: false + # body_format: json + # body: {} + + + # - name: Trigger firmware check + # ansible.builtin.uri: + # url: "https://{{ ansible_host }}/api/core/firmware/check" + # method: POST + # user: "{{ opnsense_api_key }}" + # password: "{{ opnsense_api_secret }}" + # force_basic_auth: true + # validate_certs: false + # body_format: json + # body: {} + + # - name: Wait for check to complete + # ansible.builtin.uri: + # url: "https://{{ ansible_host }}/api/core/firmware/status" + # method: GET + # user: "{{ opnsense_api_key }}" + # password: "{{ opnsense_api_secret }}" + # force_basic_auth: true + # validate_certs: false + # register: _fw_status + # until: _fw_status.json.status != 'running' + # retries: 12 + # delay: 10 + +# - name: Store firmware facts +# ansible.builtin.set_fact: +# fw_status: "{{ _fw_status.json.status }}" +# fw_current_version: "{{ _fw_status.json.product_version | default('unknown') }}" +# fw_new_version: "{{ _fw_status.json.new_version | default('') }}" +# # 'pending' means updates exist; will be overwritten to success/rolled_back +# # If it stays 'pending' after the run, a pre-update check failed +# update_result: "{{ 'pending' if _fw_status.json.status == 'update' else 'skipped' }}" + +# - name: Display firmware status +# ansible.builtin.debug: +# msg: >- +# {{ inventory_hostname }}: {{ fw_status }} +# ({{ fw_current_version }} +# {% if fw_new_version %} → {{ fw_new_version }}{% endif %}) + +# - name: Warn — major upgrade skipped, handle manually +# ansible.builtin.debug: +# msg: >- +# NOTICE: {{ inventory_hostname }} has a MAJOR upgrade available +# ({{ fw_new_version }}). This playbook handles minor updates only. +# when: fw_status == 'upgrade' + + +# ############################################################### +# - name: "Phase 2 | Update backup node (TrueNAS, no snapshot)" +# hosts: opnsense_backup +# gather_facts: false + +# tasks: +# - name: Skip — no minor updates available on backup +# ansible.builtin.meta: end_host +# when: fw_status != 'update' + +# # Safety: confirm backup is actually in BACKUP role before touching it +# - name: Verify backup node CARP role +# ansible.builtin.uri: +# url: "https://{{ ansible_host }}/api/carp/status/get" +# method: GET +# user: "{{ opnsense_api_key }}" +# password: "{{ opnsense_api_secret }}" +# force_basic_auth: true +# validate_certs: false +# register: _carp +# # Abort if node is unexpectedly MASTER — updating it would disrupt traffic +# failed_when: _carp.json.status | upper != 'BACKUP' + +# # Maintenance mode prevents the backup from attempting to take over +# # during its own reboot (avoids CARP flapping) +# - name: Enable CARP maintenance mode +# ansible.builtin.uri: +# url: "https://{{ ansible_host }}/api/carp/status/maintenancemode" +# method: POST +# user: "{{ opnsense_api_key }}" +# password: "{{ opnsense_api_secret }}" +# force_basic_auth: true +# validate_certs: false +# body_format: json +# body: {} + +# - name: Trigger firmware update +# ansible.builtin.uri: +# url: "https://{{ ansible_host }}/api/core/firmware/update" +# method: POST +# user: "{{ opnsense_api_key }}" +# password: "{{ opnsense_api_secret }}" +# force_basic_auth: true +# validate_certs: false +# body_format: json +# body: {} + +# - name: Wait for packages to install +# ansible.builtin.uri: +# url: "https://{{ ansible_host }}/api/core/firmware/running" +# method: GET +# user: "{{ opnsense_api_key }}" +# password: "{{ opnsense_api_secret }}" +# force_basic_auth: true +# validate_certs: false +# register: _running +# until: _running.json.status != 'running' +# retries: 30 +# delay: 15 + +# # Reboot is a separate explicit step — update does NOT auto-reboot +# - name: Reboot node +# ansible.builtin.uri: +# url: "https://{{ ansible_host }}/api/core/firmware/reboot" +# method: POST +# user: "{{ opnsense_api_key }}" +# password: "{{ opnsense_api_secret }}" +# force_basic_auth: true +# validate_certs: false +# body_format: json +# body: {} +# ignore_errors: true # Node drops the connection immediately + +# - name: Wait for node to go offline +# ansible.builtin.wait_for: +# host: "{{ ansible_host }}" +# port: 443 +# state: stopped +# timeout: 60 +# delegate_to: localhost + +# - name: Wait for node to come back online +# ansible.builtin.wait_for: +# host: "{{ ansible_host }}" +# port: 443 +# state: started +# timeout: 300 +# delay: 30 +# delegate_to: localhost + +# - name: Wait for API to be fully responsive +# ansible.builtin.uri: +# url: "https://{{ ansible_host }}/api/core/firmware/status" +# method: GET +# user: "{{ opnsense_api_key }}" +# password: "{{ opnsense_api_secret }}" +# force_basic_auth: true +# validate_certs: false +# register: _post_status +# until: _post_status.status == 200 +# retries: 20 +# delay: 10 + +# - name: Health check — no remaining updates +# ansible.builtin.assert: +# that: +# - _post_status.json.status == 'none' +# fail_msg: >- +# Post-update firmware status is '{{ _post_status.json.status }}' +# on {{ inventory_hostname }}. Expected 'none'. + +# - name: Health check — CARP role is BACKUP +# ansible.builtin.uri: +# url: "https://{{ ansible_host }}/api/carp/status/get" +# method: GET +# user: "{{ opnsense_api_key }}" +# password: "{{ opnsense_api_secret }}" +# force_basic_auth: true +# validate_certs: false +# register: _carp_post +# failed_when: _carp_post.json.status | upper not in ['BACKUP', 'MAINTENANCE'] + +# - name: Disable CARP maintenance mode +# ansible.builtin.uri: +# url: "https://{{ ansible_host }}/api/carp/status/maintenancemode" +# method: POST +# user: "{{ opnsense_api_key }}" +# password: "{{ opnsense_api_secret }}" +# force_basic_auth: true +# validate_certs: false +# body_format: json +# body: {} + +# - name: Record backup update result +# ansible.builtin.set_fact: +# update_result: "success" +# fw_post_version: "{{ _post_status.json.product_version | default('unknown') }}" + + +# ############################################################### +# - name: "Phase 3 | Update master node (Proxmox, with snapshot)" +# hosts: opnsense_master +# gather_facts: false + +# vars: +# snap_name: "preupdate-{{ now().strftime('%Y%m%d-%H%M') }}" +# _pve_base: "https://{{ proxmox_api_host }}/api2/json/nodes/{{ proxmox_node }}/qemu/{{ proxmox_vmid }}" +# _pve_auth: +# Authorization: "PVEAPIToken={{ proxmox_api_token_id }}={{ proxmox_api_token_secret }}" + +# tasks: +# - name: Skip — no minor updates available on master +# ansible.builtin.meta: end_host +# when: fw_status != 'update' + +# - name: Verify master node CARP role +# ansible.builtin.uri: +# url: "https://{{ ansible_host }}/api/carp/status/get" +# method: GET +# user: "{{ opnsense_api_key }}" +# password: "{{ opnsense_api_secret }}" +# force_basic_auth: true +# validate_certs: false +# register: _carp +# failed_when: _carp.json.status | upper != 'MASTER' + +# # Snapshot BEFORE maintenance mode — the restore point should be +# # a fully functioning MASTER node, not one mid-failover +# - name: Take Proxmox VM snapshot +# community.general.proxmox_snap: +# api_host: "{{ proxmox_api_host }}" +# api_token_id: "{{ proxmox_api_token_id }}" +# api_token_secret: "{{ proxmox_api_token_secret }}" +# vmid: "{{ proxmox_vmid }}" +# state: present +# snapname: "{{ snap_name }}" +# description: "Pre-firmware-update: {{ fw_current_version }} → {{ fw_new_version }}" +# timeout: 60 +# delegate_to: localhost + +# - name: Enable CARP maintenance mode (triggers controlled failover to backup) +# ansible.builtin.uri: +# url: "https://{{ ansible_host }}/api/carp/status/maintenancemode" +# method: POST +# user: "{{ opnsense_api_key }}" +# password: "{{ opnsense_api_secret }}" +# force_basic_auth: true +# validate_certs: false +# body_format: json +# body: {} + +# - name: Wait for CARP failover to settle +# ansible.builtin.pause: +# seconds: 10 + +# # Do not proceed if the backup has not actually taken over. +# # A failed failover means no traffic coverage during master update. +# - name: Confirm backup node is now MASTER +# ansible.builtin.uri: +# url: "https://{{ hostvars[groups['opnsense_backup'][0]].ansible_host }}/api/carp/status/get" +# method: GET +# user: "{{ opnsense_api_key }}" +# password: "{{ opnsense_api_secret }}" +# force_basic_auth: true +# validate_certs: false +# register: _backup_carp +# delegate_to: localhost +# failed_when: _backup_carp.json.status | upper != 'MASTER' + +# - name: Update and verify +# block: + +# - name: Trigger firmware update +# ansible.builtin.uri: +# url: "https://{{ ansible_host }}/api/core/firmware/update" +# method: POST +# user: "{{ opnsense_api_key }}" +# password: "{{ opnsense_api_secret }}" +# force_basic_auth: true +# validate_certs: false +# body_format: json +# body: {} + +# - name: Wait for packages to install +# ansible.builtin.uri: +# url: "https://{{ ansible_host }}/api/core/firmware/running" +# method: GET +# user: "{{ opnsense_api_key }}" +# password: "{{ opnsense_api_secret }}" +# force_basic_auth: true +# validate_certs: false +# register: _running +# until: _running.json.status != 'running' +# retries: 30 +# delay: 15 + +# - name: Reboot node +# ansible.builtin.uri: +# url: "https://{{ ansible_host }}/api/core/firmware/reboot" +# method: POST +# user: "{{ opnsense_api_key }}" +# password: "{{ opnsense_api_secret }}" +# force_basic_auth: true +# validate_certs: false +# body_format: json +# body: {} +# ignore_errors: true + +# - name: Wait for node to go offline +# ansible.builtin.wait_for: +# host: "{{ ansible_host }}" +# port: 443 +# state: stopped +# timeout: 60 +# delegate_to: localhost + +# - name: Wait for node to come back online +# ansible.builtin.wait_for: +# host: "{{ ansible_host }}" +# port: 443 +# state: started +# timeout: 300 +# delay: 30 +# delegate_to: localhost + +# - name: Wait for API to be fully responsive +# ansible.builtin.uri: +# url: "https://{{ ansible_host }}/api/core/firmware/status" +# method: GET +# user: "{{ opnsense_api_key }}" +# password: "{{ opnsense_api_secret }}" +# force_basic_auth: true +# validate_certs: false +# register: _post_status +# until: _post_status.status == 200 +# retries: 20 +# delay: 10 + +# - name: Health check — no remaining updates +# ansible.builtin.assert: +# that: +# - _post_status.json.status == 'none' +# fail_msg: >- +# Post-update firmware status is '{{ _post_status.json.status }}'. +# Expected 'none'. Triggering rollback. + +# # After maintenance mode failover, master should rejoin as BACKUP +# - name: Health check — CARP role is BACKUP +# ansible.builtin.uri: +# url: "https://{{ ansible_host }}/api/carp/status/get" +# method: GET +# user: "{{ opnsense_api_key }}" +# password: "{{ opnsense_api_secret }}" +# force_basic_auth: true +# validate_certs: false +# register: _carp_post +# failed_when: _carp_post.json.status | upper not in ['BACKUP', 'MAINTENANCE'] + +# - name: Record master update result +# ansible.builtin.set_fact: +# update_result: "success" +# fw_post_version: "{{ _post_status.json.product_version | default('unknown') }}" + +# rescue: +# # Snapshot was taken before maintenance mode, so the restored VM +# # will come back as a healthy MASTER on the previous version. +# # Backup (already updated) continues serving traffic during rollback. + +# - name: "ROLLBACK | Stop VM" +# ansible.builtin.uri: +# url: "{{ _pve_base }}/status/stop" +# method: POST +# headers: "{{ _pve_auth }}" +# validate_certs: false +# delegate_to: localhost + +# - name: "ROLLBACK | Wait for VM to stop" +# ansible.builtin.uri: +# url: "{{ _pve_base }}/status/current" +# method: GET +# headers: "{{ _pve_auth }}" +# validate_certs: false +# register: _vm_state +# until: _vm_state.json.data.status == 'stopped' +# retries: 12 +# delay: 5 +# delegate_to: localhost + +# - name: "ROLLBACK | Restore snapshot {{ snap_name }}" +# ansible.builtin.uri: +# url: "{{ _pve_base }}/snapshot/{{ snap_name }}/rollback" +# method: POST +# headers: "{{ _pve_auth }}" +# validate_certs: false +# delegate_to: localhost + +# # Proxmox rollback tasks run async; wait for completion +# - name: "ROLLBACK | Wait for snapshot restore to complete" +# ansible.builtin.pause: +# seconds: 30 + +# - name: "ROLLBACK | Start VM" +# ansible.builtin.uri: +# url: "{{ _pve_base }}/status/start" +# method: POST +# headers: "{{ _pve_auth }}" +# validate_certs: false +# delegate_to: localhost + +# - name: "ROLLBACK | Wait for VM to come back online" +# ansible.builtin.wait_for: +# host: "{{ ansible_host }}" +# port: 443 +# state: started +# timeout: 300 +# delay: 30 +# delegate_to: localhost + +# - name: "ROLLBACK | Record result" +# ansible.builtin.set_fact: +# update_result: "rolled_back" +# fw_post_version: "{{ fw_current_version }}" + +# always: +# # Best-effort: disable maintenance mode whether update succeeded or rolled back. +# # On rollback, the restored snapshot predates maintenance mode so this +# # is effectively a no-op, but run it anyway as a safety net. +# - name: Disable CARP maintenance mode +# ansible.builtin.uri: +# url: "https://{{ ansible_host }}/api/carp/status/maintenancemode" +# method: POST +# user: "{{ opnsense_api_key }}" +# password: "{{ opnsense_api_secret }}" +# force_basic_auth: true +# validate_certs: false +# body_format: json +# body: {} +# ignore_errors: true + + +# ############################################################### +# # Phase 4 targets localhost — it runs even if Phase 3 failed, +# # because localhost was never marked as a failed host. +# - name: "Phase 4 | Send ntfy notification" +# hosts: localhost +# gather_facts: false + +# tasks: +# - name: Build report +# ansible.builtin.set_fact: +# _report: | +# {% for host in groups['opnsense'] %} +# {% set h = hostvars[host] %} +# {{ host }} +# fw check : {{ h.fw_status | default('unknown') }} +# result : {{ h.update_result | default('unknown') }} +# version : {{ h.fw_post_version | default(h.fw_current_version | default('?')) }} +# {% endfor %} +# _any_issue: >- +# {{ groups['opnsense'] +# | map('extract', hostvars, 'update_result') +# | select('in', ['rolled_back', 'pending']) +# | list +# | length > 0 }} + +# - name: Send ntfy notification +# ansible.builtin.uri: +# url: "{{ ntfy_url }}/{{ ntfy_topic }}" +# method: POST +# headers: +# Authorization: "Bearer {{ ntfy_token }}" +# Title: "OPNsense HA Update Report" +# Priority: "{{ 'high' if _any_issue else 'default' }}" +# Tags: "{{ 'warning' if _any_issue else 'white_check_mark' }},shield" +# Content-Type: "text/plain" +# body: "{{ _report }}" + +# - name: Fail playbook if any node was rolled back or did not complete +# ansible.builtin.fail: +# msg: >- +# One or more nodes did not update successfully. +# Check ntfy report and Ansible output for details. +# when: _any_issue | bool \ No newline at end of file -- 2.54.0 From aaf111919dede27dfc7350bfd6ea944331ce13f7 Mon Sep 17 00:00:00 2001 From: Vezpi Date: Wed, 17 Jun 2026 09:17:46 +0000 Subject: [PATCH 02/74] test: validate_certs to false --- ansible/opnsense/update_opnsense_ha_cluster.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 7482be4..fb64f7a 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -17,7 +17,7 @@ user: "{{ opnsense_api_key }}" password: "{{ opnsense_api_secret }}" force_basic_auth: true - # validate_certs: false + validate_certs: false # body_format: json # body: {} -- 2.54.0 From e1b7a88d1f032de36df55dcc5b9041700c4bf14d Mon Sep 17 00:00:00 2001 From: Vezpi Date: Wed, 17 Jun 2026 09:22:25 +0000 Subject: [PATCH 03/74] test: removed vars --- ansible/opnsense/update_opnsense_ha_cluster.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index fb64f7a..f792f95 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -5,14 +5,14 @@ - name: "Phase 1 | Firmware check on all nodes" hosts: opnsense gather_facts: false - vars: - opnsense_api_key: "{{ lookup('env', 'OPNSENSE_API_KEY') }}" - opnsense_api_secret: "{{ lookup('env', 'OPNSENSE_API_SECRET') }}" + # vars: + # opnsense_api_key: "{{ lookup('env', 'OPNSENSE_API_KEY') }}" + # opnsense_api_secret: "{{ lookup('env', 'OPNSENSE_API_SECRET') }}" tasks: - name: Check node availability ansible.builtin.uri: - url: "https://{{ ansible_host }}/api/core/system/status" + url: "https://{{ opnsense_host }}:4443/api/core/system/status" method: GET user: "{{ opnsense_api_key }}" password: "{{ opnsense_api_secret }}" -- 2.54.0 From 63db96d1bd144801a298046022776c7733227ea9 Mon Sep 17 00:00:00 2001 From: Vezpi Date: Wed, 17 Jun 2026 09:23:44 +0000 Subject: [PATCH 04/74] fix: remove port --- ansible/opnsense/update_opnsense_ha_cluster.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index f792f95..4f97b3e 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -12,7 +12,7 @@ tasks: - name: Check node availability ansible.builtin.uri: - url: "https://{{ opnsense_host }}:4443/api/core/system/status" + url: "https://{{ opnsense_host }}/api/core/system/status" method: GET user: "{{ opnsense_api_key }}" password: "{{ opnsense_api_secret }}" -- 2.54.0 From b7cf1499a7153ddfe2c59f9235509a274da5e5aa Mon Sep 17 00:00:00 2001 From: Vezpi Date: Wed, 17 Jun 2026 09:30:42 +0000 Subject: [PATCH 05/74] test: unset validate certs --- ansible/opnsense/update_opnsense_ha_cluster.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 4f97b3e..ea7ec56 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -17,7 +17,7 @@ user: "{{ opnsense_api_key }}" password: "{{ opnsense_api_secret }}" force_basic_auth: true - validate_certs: false + # validate_certs: false # body_format: json # body: {} -- 2.54.0 From ca01e8f17eddd434db8640d58c5f7d630e0775dd Mon Sep 17 00:00:00 2001 From: Vezpi Date: Wed, 17 Jun 2026 09:48:40 +0000 Subject: [PATCH 06/74] Revert "test: unset validate certs" This reverts commit 3c8375d6f408e975292f25467c5881b514972ed8. --- ansible/opnsense/update_opnsense_ha_cluster.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index ea7ec56..4f97b3e 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -17,7 +17,7 @@ user: "{{ opnsense_api_key }}" password: "{{ opnsense_api_secret }}" force_basic_auth: true - # validate_certs: false + validate_certs: false # body_format: json # body: {} -- 2.54.0 From 7d51740a1a1761227a3c4d77fe25bb88e52e2534 Mon Sep 17 00:00:00 2001 From: Vezpi Date: Wed, 17 Jun 2026 19:27:15 +0000 Subject: [PATCH 07/74] add: firmware check --- .../opnsense/update_opnsense_ha_cluster.yml | 112 +++++++++--------- 1 file changed, 54 insertions(+), 58 deletions(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 4f97b3e..ec8371c 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -4,70 +4,66 @@ ############################################################### - name: "Phase 1 | Firmware check on all nodes" hosts: opnsense + any_errors_fatal: true gather_facts: false - # vars: - # opnsense_api_key: "{{ lookup('env', 'OPNSENSE_API_KEY') }}" - # opnsense_api_secret: "{{ lookup('env', 'OPNSENSE_API_SECRET') }}" tasks: - - name: Check node availability - ansible.builtin.uri: - url: "https://{{ opnsense_host }}/api/core/system/status" - method: GET - user: "{{ opnsense_api_key }}" - password: "{{ opnsense_api_secret }}" - force_basic_auth: true - validate_certs: false - # body_format: json - # body: {} + - block: - # - name: Trigger firmware check - # ansible.builtin.uri: - # url: "https://{{ ansible_host }}/api/core/firmware/check" - # method: POST - # user: "{{ opnsense_api_key }}" - # password: "{{ opnsense_api_secret }}" - # force_basic_auth: true - # validate_certs: false - # body_format: json - # body: {} + - name: Check node availability + ansible.builtin.uri: + url: "https://{{ opnsense_host }}/api/core/system/status" + method: GET + user: "{{ opnsense_api_key }}" + password: "{{ opnsense_api_secret }}" + force_basic_auth: true + validate_certs: false - # - name: Wait for check to complete - # ansible.builtin.uri: - # url: "https://{{ ansible_host }}/api/core/firmware/status" - # method: GET - # user: "{{ opnsense_api_key }}" - # password: "{{ opnsense_api_secret }}" - # force_basic_auth: true - # validate_certs: false - # register: _fw_status - # until: _fw_status.json.status != 'running' - # retries: 12 - # delay: 10 + - name: Trigger firmware check + ansible.builtin.uri: + url: "https://{{ ansible_host }}/api/core/firmware/check" + method: POST + user: "{{ opnsense_api_key }}" + password: "{{ opnsense_api_secret }}" + force_basic_auth: true + validate_certs: false -# - name: Store firmware facts -# ansible.builtin.set_fact: -# fw_status: "{{ _fw_status.json.status }}" -# fw_current_version: "{{ _fw_status.json.product_version | default('unknown') }}" -# fw_new_version: "{{ _fw_status.json.new_version | default('') }}" -# # 'pending' means updates exist; will be overwritten to success/rolled_back -# # If it stays 'pending' after the run, a pre-update check failed -# update_result: "{{ 'pending' if _fw_status.json.status == 'update' else 'skipped' }}" + - name: Wait for check to complete + ansible.builtin.uri: + url: "https://{{ ansible_host }}/api/core/firmware/status" + method: GET + user: "{{ opnsense_api_key }}" + password: "{{ opnsense_api_secret }}" + force_basic_auth: true + validate_certs: false + register: firmware_status + until: firmware_status.json.status != 'none' + retries: 12 + delay: 5 -# - name: Display firmware status -# ansible.builtin.debug: -# msg: >- -# {{ inventory_hostname }}: {{ fw_status }} -# ({{ fw_current_version }} -# {% if fw_new_version %} → {{ fw_new_version }}{% endif %}) + - name: Store firmware facts + ansible.builtin.set_fact: + firmware_status: "{{ _firmware_status.json.status }}" + fw_current_version: "{{ _firmware_status.json.product_version | default('unknown') }}" + fw_new_version: "{{ _firmware_status.json.new_version | default('') }}" + # 'pending' means updates exist; will be overwritten to success/rolled_back + # If it stays 'pending' after the run, a pre-update check failed + update_result: "{{ 'pending' if _firmware_status.json.status == 'update' else 'skipped' }}" -# - name: Warn — major upgrade skipped, handle manually -# ansible.builtin.debug: -# msg: >- -# NOTICE: {{ inventory_hostname }} has a MAJOR upgrade available -# ({{ fw_new_version }}). This playbook handles minor updates only. -# when: fw_status == 'upgrade' + - name: Display firmware status + ansible.builtin.debug: + msg: >- + {{ inventory_hostname }}: {{ firmware_status }} + ({{ fw_current_version }} + {% if fw_new_version %} → {{ fw_new_version }}{% endif %}) + + - name: Warn — major upgrade skipped, handle manually + ansible.builtin.debug: + msg: >- + NOTICE: {{ inventory_hostname }} has a MAJOR upgrade available + ({{ fw_new_version }}). This playbook handles minor updates only. + when: firmware_status == 'upgrade' # ############################################################### @@ -78,7 +74,7 @@ # tasks: # - name: Skip — no minor updates available on backup # ansible.builtin.meta: end_host -# when: fw_status != 'update' +# when: firmware_status != 'update' # # Safety: confirm backup is actually in BACKUP role before touching it # - name: Verify backup node CARP role @@ -223,7 +219,7 @@ # tasks: # - name: Skip — no minor updates available on master # ansible.builtin.meta: end_host -# when: fw_status != 'update' +# when: firmware_status != 'update' # - name: Verify master node CARP role # ansible.builtin.uri: @@ -464,7 +460,7 @@ # {% for host in groups['opnsense'] %} # {% set h = hostvars[host] %} # {{ host }} -# fw check : {{ h.fw_status | default('unknown') }} +# fw check : {{ h.firmware_status | default('unknown') }} # result : {{ h.update_result | default('unknown') }} # version : {{ h.fw_post_version | default(h.fw_current_version | default('?')) }} # {% endfor %} -- 2.54.0 From fe24fec3a346568c523b6f6705d09910040646dd Mon Sep 17 00:00:00 2001 From: Vezpi Date: Wed, 17 Jun 2026 19:34:35 +0000 Subject: [PATCH 08/74] fix: host variable for API url --- .../opnsense/update_opnsense_ha_cluster.yml | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index ec8371c..5f13288 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -22,7 +22,7 @@ - name: Trigger firmware check ansible.builtin.uri: - url: "https://{{ ansible_host }}/api/core/firmware/check" + url: "https://{{ opnsense_host }}/api/core/firmware/check" method: POST user: "{{ opnsense_api_key }}" password: "{{ opnsense_api_secret }}" @@ -31,7 +31,7 @@ - name: Wait for check to complete ansible.builtin.uri: - url: "https://{{ ansible_host }}/api/core/firmware/status" + url: "https://{{ opnsense_host }}/api/core/firmware/status" method: GET user: "{{ opnsense_api_key }}" password: "{{ opnsense_api_secret }}" @@ -79,7 +79,7 @@ # # Safety: confirm backup is actually in BACKUP role before touching it # - name: Verify backup node CARP role # ansible.builtin.uri: -# url: "https://{{ ansible_host }}/api/carp/status/get" +# url: "https://{{ opnsense_host }}/api/carp/status/get" # method: GET # user: "{{ opnsense_api_key }}" # password: "{{ opnsense_api_secret }}" @@ -93,7 +93,7 @@ # # during its own reboot (avoids CARP flapping) # - name: Enable CARP maintenance mode # ansible.builtin.uri: -# url: "https://{{ ansible_host }}/api/carp/status/maintenancemode" +# url: "https://{{ opnsense_host }}/api/carp/status/maintenancemode" # method: POST # user: "{{ opnsense_api_key }}" # password: "{{ opnsense_api_secret }}" @@ -104,7 +104,7 @@ # - name: Trigger firmware update # ansible.builtin.uri: -# url: "https://{{ ansible_host }}/api/core/firmware/update" +# url: "https://{{ opnsense_host }}/api/core/firmware/update" # method: POST # user: "{{ opnsense_api_key }}" # password: "{{ opnsense_api_secret }}" @@ -115,7 +115,7 @@ # - name: Wait for packages to install # ansible.builtin.uri: -# url: "https://{{ ansible_host }}/api/core/firmware/running" +# url: "https://{{ opnsense_host }}/api/core/firmware/running" # method: GET # user: "{{ opnsense_api_key }}" # password: "{{ opnsense_api_secret }}" @@ -129,7 +129,7 @@ # # Reboot is a separate explicit step — update does NOT auto-reboot # - name: Reboot node # ansible.builtin.uri: -# url: "https://{{ ansible_host }}/api/core/firmware/reboot" +# url: "https://{{ opnsense_host }}/api/core/firmware/reboot" # method: POST # user: "{{ opnsense_api_key }}" # password: "{{ opnsense_api_secret }}" @@ -158,7 +158,7 @@ # - name: Wait for API to be fully responsive # ansible.builtin.uri: -# url: "https://{{ ansible_host }}/api/core/firmware/status" +# url: "https://{{ opnsense_host }}/api/core/firmware/status" # method: GET # user: "{{ opnsense_api_key }}" # password: "{{ opnsense_api_secret }}" @@ -179,7 +179,7 @@ # - name: Health check — CARP role is BACKUP # ansible.builtin.uri: -# url: "https://{{ ansible_host }}/api/carp/status/get" +# url: "https://{{ opnsense_host }}/api/carp/status/get" # method: GET # user: "{{ opnsense_api_key }}" # password: "{{ opnsense_api_secret }}" @@ -190,7 +190,7 @@ # - name: Disable CARP maintenance mode # ansible.builtin.uri: -# url: "https://{{ ansible_host }}/api/carp/status/maintenancemode" +# url: "https://{{ opnsense_host }}/api/carp/status/maintenancemode" # method: POST # user: "{{ opnsense_api_key }}" # password: "{{ opnsense_api_secret }}" @@ -223,7 +223,7 @@ # - name: Verify master node CARP role # ansible.builtin.uri: -# url: "https://{{ ansible_host }}/api/carp/status/get" +# url: "https://{{ opnsense_host }}/api/carp/status/get" # method: GET # user: "{{ opnsense_api_key }}" # password: "{{ opnsense_api_secret }}" @@ -248,7 +248,7 @@ # - name: Enable CARP maintenance mode (triggers controlled failover to backup) # ansible.builtin.uri: -# url: "https://{{ ansible_host }}/api/carp/status/maintenancemode" +# url: "https://{{ opnsense_host }}/api/carp/status/maintenancemode" # method: POST # user: "{{ opnsense_api_key }}" # password: "{{ opnsense_api_secret }}" @@ -280,7 +280,7 @@ # - name: Trigger firmware update # ansible.builtin.uri: -# url: "https://{{ ansible_host }}/api/core/firmware/update" +# url: "https://{{ opnsense_host }}/api/core/firmware/update" # method: POST # user: "{{ opnsense_api_key }}" # password: "{{ opnsense_api_secret }}" @@ -291,7 +291,7 @@ # - name: Wait for packages to install # ansible.builtin.uri: -# url: "https://{{ ansible_host }}/api/core/firmware/running" +# url: "https://{{ opnsense_host }}/api/core/firmware/running" # method: GET # user: "{{ opnsense_api_key }}" # password: "{{ opnsense_api_secret }}" @@ -304,7 +304,7 @@ # - name: Reboot node # ansible.builtin.uri: -# url: "https://{{ ansible_host }}/api/core/firmware/reboot" +# url: "https://{{ opnsense_host }}/api/core/firmware/reboot" # method: POST # user: "{{ opnsense_api_key }}" # password: "{{ opnsense_api_secret }}" @@ -333,7 +333,7 @@ # - name: Wait for API to be fully responsive # ansible.builtin.uri: -# url: "https://{{ ansible_host }}/api/core/firmware/status" +# url: "https://{{ opnsense_host }}/api/core/firmware/status" # method: GET # user: "{{ opnsense_api_key }}" # password: "{{ opnsense_api_secret }}" @@ -355,7 +355,7 @@ # # After maintenance mode failover, master should rejoin as BACKUP # - name: Health check — CARP role is BACKUP # ansible.builtin.uri: -# url: "https://{{ ansible_host }}/api/carp/status/get" +# url: "https://{{ opnsense_host }}/api/carp/status/get" # method: GET # user: "{{ opnsense_api_key }}" # password: "{{ opnsense_api_secret }}" @@ -435,7 +435,7 @@ # # is effectively a no-op, but run it anyway as a safety net. # - name: Disable CARP maintenance mode # ansible.builtin.uri: -# url: "https://{{ ansible_host }}/api/carp/status/maintenancemode" +# url: "https://{{ opnsense_host }}/api/carp/status/maintenancemode" # method: POST # user: "{{ opnsense_api_key }}" # password: "{{ opnsense_api_secret }}" -- 2.54.0 From 6e3814ce3f6db6b2947a8b4d8ea4a5b31ae96ed9 Mon Sep 17 00:00:00 2001 From: Vezpi Date: Wed, 17 Jun 2026 19:37:09 +0000 Subject: [PATCH 09/74] fix: var name --- ansible/opnsense/update_opnsense_ha_cluster.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 5f13288..b2bf028 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -44,12 +44,12 @@ - name: Store firmware facts ansible.builtin.set_fact: - firmware_status: "{{ _firmware_status.json.status }}" - fw_current_version: "{{ _firmware_status.json.product_version | default('unknown') }}" - fw_new_version: "{{ _firmware_status.json.new_version | default('') }}" + firmware_status: "{{ firmware_status.json.status }}" + fw_current_version: "{{ firmware_status.json.product_version | default('unknown') }}" + fw_new_version: "{{ firmware_status.json.new_version | default('') }}" # 'pending' means updates exist; will be overwritten to success/rolled_back # If it stays 'pending' after the run, a pre-update check failed - update_result: "{{ 'pending' if _firmware_status.json.status == 'update' else 'skipped' }}" + update_result: "{{ 'pending' if firmware_status.json.status == 'update' else 'skipped' }}" - name: Display firmware status ansible.builtin.debug: -- 2.54.0 From d059b86dfc635852d14a9352ab7bc8f12c4301b9 Mon Sep 17 00:00:00 2001 From: Vezpi Date: Wed, 17 Jun 2026 20:46:25 +0000 Subject: [PATCH 10/74] fix: variables --- ansible/opnsense/update_opnsense_ha_cluster.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index b2bf028..a6b7671 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -37,19 +37,20 @@ password: "{{ opnsense_api_secret }}" force_basic_auth: true validate_certs: false - register: firmware_status - until: firmware_status.json.status != 'none' + register: _firmware_status + until: _firmware_status.json.status != 'none' retries: 12 delay: 5 - name: Store firmware facts ansible.builtin.set_fact: - firmware_status: "{{ firmware_status.json.status }}" - fw_current_version: "{{ firmware_status.json.product_version | default('unknown') }}" - fw_new_version: "{{ firmware_status.json.new_version | default('') }}" + firmware_status: "{{ _firmware_status.json.status }}" + fw_current_version: "{{ _firmware_status.json.product_version | default('unknown') }}" + fw_new_version: "{{ _firmware_status.json.upgrade_packages | selecattr('name', 'equalto', 'opnsense') | map(attribute='new_version') | first | default('') }}" + needs_reboot: "{{ true if _firmware_status.json.needs_reboot == '1' else false }}" # 'pending' means updates exist; will be overwritten to success/rolled_back # If it stays 'pending' after the run, a pre-update check failed - update_result: "{{ 'pending' if firmware_status.json.status == 'update' else 'skipped' }}" + update_result: "{{ 'pending' if _firmware_status.json.status == 'update' else 'skipped' }}" - name: Display firmware status ansible.builtin.debug: -- 2.54.0 From 02b908c4cdc8e66cc74dea78e576fcaa0df11ec8 Mon Sep 17 00:00:00 2001 From: Vezpi Date: Wed, 17 Jun 2026 20:47:18 +0000 Subject: [PATCH 11/74] fix: typo --- ansible/opnsense/update_opnsense_ha_cluster.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index a6b7671..075a31c 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -46,7 +46,7 @@ ansible.builtin.set_fact: firmware_status: "{{ _firmware_status.json.status }}" fw_current_version: "{{ _firmware_status.json.product_version | default('unknown') }}" - fw_new_version: "{{ _firmware_status.json.upgrade_packages | selecattr('name', 'equalto', 'opnsense') | map(attribute='new_version') | first | default('') }}" + fw_new_version: "{{ _firmware_status.json.upgrade_packages | selectattr('name', 'equalto', 'opnsense') | map(attribute='new_version') | first | default('') }}" needs_reboot: "{{ true if _firmware_status.json.needs_reboot == '1' else false }}" # 'pending' means updates exist; will be overwritten to success/rolled_back # If it stays 'pending' after the run, a pre-update check failed -- 2.54.0 From 12f3c65c3927d21dd260bc80ac3606e23a56c1c1 Mon Sep 17 00:00:00 2001 From: Vezpi Date: Wed, 17 Jun 2026 20:50:28 +0000 Subject: [PATCH 12/74] add: reboot info --- ansible/opnsense/update_opnsense_ha_cluster.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 075a31c..a7bc4a0 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -57,7 +57,7 @@ msg: >- {{ inventory_hostname }}: {{ firmware_status }} ({{ fw_current_version }} - {% if fw_new_version %} → {{ fw_new_version }}{% endif %}) + {% if fw_new_version %} → {{ fw_new_version }} (reboot: {{ needs_reboot }}){% endif %}) - name: Warn — major upgrade skipped, handle manually ansible.builtin.debug: -- 2.54.0 From 755e736ba95d36fdd6d9e03cc8a3691f80ef4a27 Mon Sep 17 00:00:00 2001 From: Vezpi Date: Wed, 17 Jun 2026 21:13:31 +0000 Subject: [PATCH 13/74] add: get vip status --- ansible/opnsense/update_opnsense_ha_cluster.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index a7bc4a0..9f0d72f 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -29,6 +29,16 @@ force_basic_auth: true validate_certs: false + - name: Check VIP status + ansible.builtin.uri: + url: "https://{{ opnsense_host }}/api/diagnostics/interface/get_vip_status" + method: GET + user: "{{ opnsense_api_key }}" + password: "{{ opnsense_api_secret }}" + force_basic_auth: true + validate_certs: false + register: _vip_status + - name: Wait for check to complete ansible.builtin.uri: url: "https://{{ opnsense_host }}/api/core/firmware/status" -- 2.54.0 From 2e912c7ad54a30048c757f4768428addc419d73f Mon Sep 17 00:00:00 2001 From: Vezpi Date: Thu, 18 Jun 2026 18:52:33 +0000 Subject: [PATCH 14/74] test: inventory var --- ansible/opnsense/update_opnsense_ha_cluster.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 9f0d72f..5381152 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -65,7 +65,7 @@ - name: Display firmware status ansible.builtin.debug: msg: >- - {{ inventory_hostname }}: {{ firmware_status }} + {{ inventory_hostname }} ({{ main_role }}): {{ firmware_status }} ({{ fw_current_version }} {% if fw_new_version %} → {{ fw_new_version }} (reboot: {{ needs_reboot }}){% endif %}) -- 2.54.0 From 413694f47de9df7a52d0bc47cf396cea88040f1e Mon Sep 17 00:00:00 2001 From: Vezpi Date: Thu, 18 Jun 2026 20:33:10 +0000 Subject: [PATCH 15/74] add: start phase 2 --- .../opnsense/update_opnsense_ha_cluster.yml | 69 ++++++++----------- 1 file changed, 28 insertions(+), 41 deletions(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 5381152..9375635 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -6,7 +6,6 @@ hosts: opnsense any_errors_fatal: true gather_facts: false - tasks: - block: @@ -58,10 +57,19 @@ fw_current_version: "{{ _firmware_status.json.product_version | default('unknown') }}" fw_new_version: "{{ _firmware_status.json.upgrade_packages | selectattr('name', 'equalto', 'opnsense') | map(attribute='new_version') | first | default('') }}" needs_reboot: "{{ true if _firmware_status.json.needs_reboot == '1' else false }}" - # 'pending' means updates exist; will be overwritten to success/rolled_back - # If it stays 'pending' after the run, a pre-update check failed + total_vips: "{{ _vip_status.json.rowCount }}" + mismatched_vips: "{{ _vip_status.json.rows | rejectattr('status', 'equalto', expected_role) | list | length }}" + in_maintenance: "{{ _vip_status.json.carp.maintenancemode }}" update_result: "{{ 'pending' if _firmware_status.json.status == 'update' else 'skipped' }}" + - name: "Assert all VIPs are {{ expected_role }} and CARP is active (no maintenance)" + ansible.builtin.assert: + that: + - firmware_status == 'update' + - total_vips > 0 + - mismatched_vips == 0 + - not in_maintenance + - name: Display firmware status ansible.builtin.debug: msg: >- @@ -69,49 +77,28 @@ ({{ fw_current_version }} {% if fw_new_version %} → {{ fw_new_version }} (reboot: {{ needs_reboot }}){% endif %}) - - name: Warn — major upgrade skipped, handle manually + rescue: + + - name: Send notification ansible.builtin.debug: - msg: >- - NOTICE: {{ inventory_hostname }} has a MAJOR upgrade available - ({{ fw_new_version }}). This playbook handles minor updates only. - when: firmware_status == 'upgrade' + msg: "End of workflow, sending notification" -# ############################################################### -# - name: "Phase 2 | Update backup node (TrueNAS, no snapshot)" -# hosts: opnsense_backup -# gather_facts: false +- name: "Phase 2 | Update backup node (TrueNAS, no snapshot)" + hosts: opnsense_backup + gather_facts: false + tasks: -# tasks: -# - name: Skip — no minor updates available on backup -# ansible.builtin.meta: end_host -# when: firmware_status != 'update' + - block: -# # Safety: confirm backup is actually in BACKUP role before touching it -# - name: Verify backup node CARP role -# ansible.builtin.uri: -# url: "https://{{ opnsense_host }}/api/carp/status/get" -# method: GET -# user: "{{ opnsense_api_key }}" -# password: "{{ opnsense_api_secret }}" -# force_basic_auth: true -# validate_certs: false -# register: _carp -# # Abort if node is unexpectedly MASTER — updating it would disrupt traffic -# failed_when: _carp.json.status | upper != 'BACKUP' - -# # Maintenance mode prevents the backup from attempting to take over -# # during its own reboot (avoids CARP flapping) -# - name: Enable CARP maintenance mode -# ansible.builtin.uri: -# url: "https://{{ opnsense_host }}/api/carp/status/maintenancemode" -# method: POST -# user: "{{ opnsense_api_key }}" -# password: "{{ opnsense_api_secret }}" -# force_basic_auth: true -# validate_certs: false -# body_format: json -# body: {} + - name: Enable CARP maintenance mode + ansible.builtin.uri: + url: "https://{{ opnsense_host }}/api/diagnostics/interface/carp_status/maintenance" + method: POST + user: "{{ opnsense_api_key }}" + password: "{{ opnsense_api_secret }}" + force_basic_auth: true + validate_certs: false # - name: Trigger firmware update # ansible.builtin.uri: -- 2.54.0 From fcf5753f57d2765a4be927a71067993cc47b3bac Mon Sep 17 00:00:00 2001 From: Vezpi Date: Thu, 18 Jun 2026 20:36:22 +0000 Subject: [PATCH 16/74] fix: variable --- ansible/opnsense/update_opnsense_ha_cluster.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 9375635..9a0a75f 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -58,11 +58,11 @@ fw_new_version: "{{ _firmware_status.json.upgrade_packages | selectattr('name', 'equalto', 'opnsense') | map(attribute='new_version') | first | default('') }}" needs_reboot: "{{ true if _firmware_status.json.needs_reboot == '1' else false }}" total_vips: "{{ _vip_status.json.rowCount }}" - mismatched_vips: "{{ _vip_status.json.rows | rejectattr('status', 'equalto', expected_role) | list | length }}" + mismatched_vips: "{{ _vip_status.json.rows | rejectattr('status', 'equalto', main_role) | list | length }}" in_maintenance: "{{ _vip_status.json.carp.maintenancemode }}" update_result: "{{ 'pending' if _firmware_status.json.status == 'update' else 'skipped' }}" - - name: "Assert all VIPs are {{ expected_role }} and CARP is active (no maintenance)" + - name: "Assert" ansible.builtin.assert: that: - firmware_status == 'update' -- 2.54.0 From 80d9befa6a0cb14695d8ba332ba2b4571d8ba791 Mon Sep 17 00:00:00 2001 From: Vezpi Date: Thu, 18 Jun 2026 20:42:14 +0000 Subject: [PATCH 17/74] add: register when enable maintenance --- ansible/opnsense/update_opnsense_ha_cluster.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 9a0a75f..ca87714 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -69,6 +69,7 @@ - total_vips > 0 - mismatched_vips == 0 - not in_maintenance + quiet: true - name: Display firmware status ansible.builtin.debug: @@ -83,6 +84,8 @@ ansible.builtin.debug: msg: "End of workflow, sending notification" + - fail: + - name: "Phase 2 | Update backup node (TrueNAS, no snapshot)" hosts: opnsense_backup @@ -99,6 +102,10 @@ password: "{{ opnsense_api_secret }}" force_basic_auth: true validate_certs: false + register: _enable_maintenance + + - debug: + var: _enable_maintenance # - name: Trigger firmware update # ansible.builtin.uri: -- 2.54.0 From e2a1e53082d23eb4b912d94b24d52b9807c94f2a Mon Sep 17 00:00:00 2001 From: Vezpi Date: Thu, 18 Jun 2026 20:46:24 +0000 Subject: [PATCH 18/74] add: changed_when --- ansible/opnsense/update_opnsense_ha_cluster.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index ca87714..7875b1e 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -103,6 +103,7 @@ force_basic_auth: true validate_certs: false register: _enable_maintenance + changed_when: _enable_maintenance.status == "ok" - debug: var: _enable_maintenance -- 2.54.0 From 0912c0a25351e2f617313d2c343b514192d6a531 Mon Sep 17 00:00:00 2001 From: Vezpi Date: Thu, 18 Jun 2026 21:27:25 +0000 Subject: [PATCH 19/74] add: ntfy notification for pre-check --- .../opnsense/update_opnsense_ha_cluster.yml | 53 ++++++++++++++----- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 7875b1e..4037bb2 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -54,8 +54,9 @@ - name: Store firmware facts ansible.builtin.set_fact: firmware_status: "{{ _firmware_status.json.status }}" - fw_current_version: "{{ _firmware_status.json.product_version | default('unknown') }}" - fw_new_version: "{{ _firmware_status.json.upgrade_packages | selectattr('name', 'equalto', 'opnsense') | map(attribute='new_version') | first | default('') }}" + firmware_current_version: "{{ _firmware_status.json.product_version | default('unknown') }}" + firmware_new_version: "{{ _firmware_status.json.upgrade_packages | selectattr('name', 'equalto', 'opnsense') | map(attribute='new_version') | first | default('') }}" + firmware_upgrade_version: "{{ _firmware_status.json.upgrade_major_version | default('unknown') }}" needs_reboot: "{{ true if _firmware_status.json.needs_reboot == '1' else false }}" total_vips: "{{ _vip_status.json.rowCount }}" mismatched_vips: "{{ _vip_status.json.rows | rejectattr('status', 'equalto', main_role) | list | length }}" @@ -66,25 +67,51 @@ ansible.builtin.assert: that: - firmware_status == 'update' - - total_vips > 0 - - mismatched_vips == 0 - not in_maintenance + - mismatched_vips == 0 + - total_vips > 0 quiet: true - name: Display firmware status ansible.builtin.debug: msg: >- {{ inventory_hostname }} ({{ main_role }}): {{ firmware_status }} - ({{ fw_current_version }} - {% if fw_new_version %} → {{ fw_new_version }} (reboot: {{ needs_reboot }}){% endif %}) + ({{ firmware_current_version }} + {% if firmware_new_version %} → {{ firmware_new_version }} (reboot: {{ needs_reboot }}){% endif %}) rescue: - - name: Send notification - ansible.builtin.debug: - msg: "End of workflow, sending notification" + - name: Send Ntfy notification + ansible.builtin.uri: + url: "{{ ntfy_url }}/{{ ntfy_topic }}" + method: POST + user: "{{ ntfy_user }}" + password: "{{ lookup('env', 'NTFY_PASSWORD') }}" + force_basic_auth: true + body: | + Current version: {{ firmware_current_version }} + {% if firmware_status != "update" %} + {% if firmware_status == "upgrade" %} + Upgrade to {{ firmware_upgrade_version }} available, relaunch the playbook manually to proceed. + {% else %} + No updates available on the cluster. + {% endif %} + {% elif in_maintenance %} + The node {{ ansible_host }} is in CARP maintenance mode. + {% elif mismatched_vips > 0 %} + Some VIP are not {{ main_role }} on {{ ansible_host }}. + {% else %} + No VIPs are managed. + {% endif %} + headers: + Title: "OPNsense update aborted" + Priority: "default" + Tags: "x" + run_once: true + when: ntfy_url is defined - - fail: + - ansible.builtin.fail: + msg: "Update aborted" - name: "Phase 2 | Update backup node (TrueNAS, no snapshot)" @@ -248,7 +275,7 @@ # vmid: "{{ proxmox_vmid }}" # state: present # snapname: "{{ snap_name }}" -# description: "Pre-firmware-update: {{ fw_current_version }} → {{ fw_new_version }}" +# description: "Pre-firmware-update: {{ firmware_current_version }} → {{ firmware_new_version }}" # timeout: 60 # delegate_to: localhost @@ -433,7 +460,7 @@ # - name: "ROLLBACK | Record result" # ansible.builtin.set_fact: # update_result: "rolled_back" -# fw_post_version: "{{ fw_current_version }}" +# fw_post_version: "{{ firmware_current_version }}" # always: # # Best-effort: disable maintenance mode whether update succeeded or rolled back. @@ -468,7 +495,7 @@ # {{ host }} # fw check : {{ h.firmware_status | default('unknown') }} # result : {{ h.update_result | default('unknown') }} -# version : {{ h.fw_post_version | default(h.fw_current_version | default('?')) }} +# version : {{ h.fw_post_version | default(h.firmware_current_version | default('?')) }} # {% endfor %} # _any_issue: >- # {{ groups['opnsense'] -- 2.54.0 From 9c49b131e2d4ecf191909c34555da466d03b9045 Mon Sep 17 00:00:00 2001 From: Vezpi Date: Fri, 19 Jun 2026 12:38:36 +0000 Subject: [PATCH 20/74] fix: user ansible_inventory instead of host --- ansible/opnsense/update_opnsense_ha_cluster.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 4037bb2..748a9c5 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -97,7 +97,7 @@ No updates available on the cluster. {% endif %} {% elif in_maintenance %} - The node {{ ansible_host }} is in CARP maintenance mode. + The node {{ ansible_inventory }} is in CARP maintenance mode. {% elif mismatched_vips > 0 %} Some VIP are not {{ main_role }} on {{ ansible_host }}. {% else %} -- 2.54.0 From de745d74dc9c32b513cdd732850369c7fc19d942 Mon Sep 17 00:00:00 2001 From: Vezpi Date: Fri, 19 Jun 2026 12:40:30 +0000 Subject: [PATCH 21/74] fix: use inventory_hostname --- ansible/opnsense/update_opnsense_ha_cluster.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 748a9c5..a8b5635 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -97,7 +97,7 @@ No updates available on the cluster. {% endif %} {% elif in_maintenance %} - The node {{ ansible_inventory }} is in CARP maintenance mode. + The node {{ inventory_hostname }} is in CARP maintenance mode. {% elif mismatched_vips > 0 %} Some VIP are not {{ main_role }} on {{ ansible_host }}. {% else %} -- 2.54.0 From 03a1b243bc0d7758702b2e02d1b152071043d5dd Mon Sep 17 00:00:00 2001 From: Vezpi Date: Fri, 19 Jun 2026 12:51:08 +0000 Subject: [PATCH 22/74] add: firmware update trigger --- .../opnsense/update_opnsense_ha_cluster.yml | 49 +++++++++---------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index a8b5635..d41c47f 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -132,32 +132,27 @@ register: _enable_maintenance changed_when: _enable_maintenance.status == "ok" - - debug: - var: _enable_maintenance + - name: Trigger firmware update + ansible.builtin.uri: + url: "https://{{ opnsense_host }}/api/core/firmware/update" + method: POST + user: "{{ opnsense_api_key }}" + password: "{{ opnsense_api_secret }}" + force_basic_auth: true + validate_certs: false -# - name: Trigger firmware update -# ansible.builtin.uri: -# url: "https://{{ opnsense_host }}/api/core/firmware/update" -# method: POST -# user: "{{ opnsense_api_key }}" -# password: "{{ opnsense_api_secret }}" -# force_basic_auth: true -# validate_certs: false -# body_format: json -# body: {} - -# - name: Wait for packages to install -# ansible.builtin.uri: -# url: "https://{{ opnsense_host }}/api/core/firmware/running" -# method: GET -# user: "{{ opnsense_api_key }}" -# password: "{{ opnsense_api_secret }}" -# force_basic_auth: true -# validate_certs: false -# register: _running -# until: _running.json.status != 'running' -# retries: 30 -# delay: 15 + - name: Wait for packages to update + ansible.builtin.uri: + url: "https://{{ opnsense_host }}/api/core/firmware/running" + method: GET + user: "{{ opnsense_api_key }}" + password: "{{ opnsense_api_secret }}" + force_basic_auth: true + validate_certs: false + register: _update_running + until: _update_running.json.status != 'running' + retries: 300 + delay: 15 # # Reboot is a separate explicit step — update does NOT auto-reboot # - name: Reboot node @@ -330,8 +325,8 @@ # password: "{{ opnsense_api_secret }}" # force_basic_auth: true # validate_certs: false -# register: _running -# until: _running.json.status != 'running' +# register: _update_running +# until: _update_running.json.status != 'running' # retries: 30 # delay: 15 -- 2.54.0 From f672517cc8a51fcdbed47985aaa18bf21ee53b33 Mon Sep 17 00:00:00 2001 From: Vezpi Date: Fri, 19 Jun 2026 13:59:28 +0000 Subject: [PATCH 23/74] add: skip backuo --- .../opnsense/update_opnsense_ha_cluster.yml | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index d41c47f..db1595e 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -61,7 +61,15 @@ total_vips: "{{ _vip_status.json.rowCount }}" mismatched_vips: "{{ _vip_status.json.rows | rejectattr('status', 'equalto', main_role) | list | length }}" in_maintenance: "{{ _vip_status.json.carp.maintenancemode }}" - update_result: "{{ 'pending' if _firmware_status.json.status == 'update' else 'skipped' }}" + # update_result: "{{ 'pending' if _firmware_status.json.status == 'update' else 'skipped' }}" + + - name: Skip backup update + ansible.builtin.set_fact: + skip_update: true + delegate_to: "{{ groups['opnsense_backup'][0] }}" + delegate_facts: true + run_once: true + when: hostvars[groups['opnsense_backup'][0]].firmware_current_version == hostvars[groups['opnsense_master'][0]].firmware_new_version - name: "Assert" ansible.builtin.assert: @@ -150,23 +158,10 @@ force_basic_auth: true validate_certs: false register: _update_running - until: _update_running.json.status != 'running' + until: _update_running.json.status != 'busy' retries: 300 delay: 15 -# # Reboot is a separate explicit step — update does NOT auto-reboot -# - name: Reboot node -# ansible.builtin.uri: -# url: "https://{{ opnsense_host }}/api/core/firmware/reboot" -# method: POST -# user: "{{ opnsense_api_key }}" -# password: "{{ opnsense_api_secret }}" -# force_basic_auth: true -# validate_certs: false -# body_format: json -# body: {} -# ignore_errors: true # Node drops the connection immediately - # - name: Wait for node to go offline # ansible.builtin.wait_for: # host: "{{ ansible_host }}" -- 2.54.0 From fbf38991d7a7820ceb1cb8d97971c99c3fc0107e Mon Sep 17 00:00:00 2001 From: Vezpi Date: Fri, 19 Jun 2026 20:58:36 +0000 Subject: [PATCH 24/74] add: master update with Proxmox snapshoty --- .../opnsense/update_opnsense_ha_cluster.yml | 534 +++++++----------- 1 file changed, 205 insertions(+), 329 deletions(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index db1595e..2893ac0 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -74,7 +74,7 @@ - name: "Assert" ansible.builtin.assert: that: - - firmware_status == 'update' + - firmware_status == 'update' or skip_update - not in_maintenance - mismatched_vips == 0 - total_vips > 0 @@ -127,7 +127,8 @@ gather_facts: false tasks: - - block: + - when: not skip_update | default(false) + block: - name: Enable CARP maintenance mode ansible.builtin.uri: @@ -162,353 +163,228 @@ retries: 300 delay: 15 -# - name: Wait for node to go offline -# ansible.builtin.wait_for: -# host: "{{ ansible_host }}" -# port: 443 -# state: stopped -# timeout: 60 -# delegate_to: localhost + - name: Wait for node to go offline + ansible.builtin.wait_for: + host: "{{ ansible_host }}" + port: "{{ opnsense_https_port }}" + state: stopped + timeout: 60 + when: needs_reboot -# - name: Wait for node to come back online -# ansible.builtin.wait_for: -# host: "{{ ansible_host }}" -# port: 443 -# state: started -# timeout: 300 -# delay: 30 -# delegate_to: localhost + - name: Wait for node to come back online + ansible.builtin.wait_for: + host: "{{ ansible_host }}" + port: "{{ opnsense_https_port }}" + state: started + timeout: 300 + delay: 30 + when: needs_reboot -# - name: Wait for API to be fully responsive -# ansible.builtin.uri: -# url: "https://{{ opnsense_host }}/api/core/firmware/status" -# method: GET -# user: "{{ opnsense_api_key }}" -# password: "{{ opnsense_api_secret }}" -# force_basic_auth: true -# validate_certs: false -# register: _post_status -# until: _post_status.status == 200 -# retries: 20 -# delay: 10 + - name: Wait for API to be fully responsive + ansible.builtin.uri: + url: "https://{{ opnsense_host }}/api/core/firmware/status" + method: GET + user: "{{ opnsense_api_key }}" + password: "{{ opnsense_api_secret }}" + force_basic_auth: true + validate_certs: false + register: _post_status + until: _post_status.status == 200 + retries: 20 + delay: 10 -# - name: Health check — no remaining updates -# ansible.builtin.assert: -# that: -# - _post_status.json.status == 'none' -# fail_msg: >- -# Post-update firmware status is '{{ _post_status.json.status }}' -# on {{ inventory_hostname }}. Expected 'none'. + - name: Check VIP status + ansible.builtin.uri: + url: "https://{{ opnsense_host }}/api/diagnostics/interface/get_vip_status" + method: GET + user: "{{ opnsense_api_key }}" + password: "{{ opnsense_api_secret }}" + force_basic_auth: true + validate_certs: false + register: _carp_post_udapte -# - name: Health check — CARP role is BACKUP -# ansible.builtin.uri: -# url: "https://{{ opnsense_host }}/api/carp/status/get" -# method: GET -# user: "{{ opnsense_api_key }}" -# password: "{{ opnsense_api_secret }}" -# force_basic_auth: true -# validate_certs: false -# register: _carp_post -# failed_when: _carp_post.json.status | upper not in ['BACKUP', 'MAINTENANCE'] + - name: Disable CARP maintenance mode + ansible.builtin.uri: + url: "https://{{ opnsense_host }}/api/carp/status/maintenancemode" + method: POST + user: "{{ opnsense_api_key }}" + password: "{{ opnsense_api_secret }}" + force_basic_auth: true + validate_certs: false + when: _carp_post_udapte.json.carp.maintenancemode -# - name: Disable CARP maintenance mode -# ansible.builtin.uri: -# url: "https://{{ opnsense_host }}/api/carp/status/maintenancemode" -# method: POST -# user: "{{ opnsense_api_key }}" -# password: "{{ opnsense_api_secret }}" -# force_basic_auth: true -# validate_certs: false -# body_format: json -# body: {} + rescue: -# - name: Record backup update result -# ansible.builtin.set_fact: -# update_result: "success" -# fw_post_version: "{{ _post_status.json.product_version | default('unknown') }}" + - name: Send Ntfy notification + ansible.builtin.uri: + url: "{{ ntfy_url }}/{{ ntfy_topic }}" + method: POST + user: "{{ ntfy_user }}" + password: "{{ lookup('env', 'NTFY_PASSWORD') }}" + force_basic_auth: true + body: | + TODO + # Current version: {{ firmware_current_version }} + # {% if firmware_status != "update" %} + # {% if firmware_status == "upgrade" %} + # Upgrade to {{ firmware_upgrade_version }} available, relaunch the playbook manually to proceed. + # {% else %} + # No updates available on the cluster. + # {% endif %} + # {% elif in_maintenance %} + # The node {{ inventory_hostname }} is in CARP maintenance mode. + # {% elif mismatched_vips > 0 %} + # Some VIP are not {{ main_role }} on {{ ansible_host }}. + # {% else %} + # No VIPs are managed. + # {% endif %} + headers: + Title: "OPNsense update failed on {{ inventory_hostname }}" + Priority: "default" + Tags: "x" + run_once: true + when: ntfy_url is defined + - ansible.builtin.fail: + msg: "Update aborted" -# ############################################################### -# - name: "Phase 3 | Update master node (Proxmox, with snapshot)" -# hosts: opnsense_master -# gather_facts: false +- name: "Phase 3 | Update master node (Proxmox, with snapshot)" + hosts: opnsense_master + gather_facts: false + vars: + proxmox_snap_name: "preupdate-{{ now().strftime('%Y%m%d-%H%M') }}" -# vars: -# snap_name: "preupdate-{{ now().strftime('%Y%m%d-%H%M') }}" -# _pve_base: "https://{{ proxmox_api_host }}/api2/json/nodes/{{ proxmox_node }}/qemu/{{ proxmox_vmid }}" -# _pve_auth: -# Authorization: "PVEAPIToken={{ proxmox_api_token_id }}={{ proxmox_api_token_secret }}" + tasks: -# tasks: -# - name: Skip — no minor updates available on master -# ansible.builtin.meta: end_host -# when: firmware_status != 'update' + - block: -# - name: Verify master node CARP role -# ansible.builtin.uri: -# url: "https://{{ opnsense_host }}/api/carp/status/get" -# method: GET -# user: "{{ opnsense_api_key }}" -# password: "{{ opnsense_api_secret }}" -# force_basic_auth: true -# validate_certs: false -# register: _carp -# failed_when: _carp.json.status | upper != 'MASTER' + - name: Take Proxmox VM snapshot + community.general.proxmox_snap: + vmid: "{{ proxmox_vmid }}" + state: present + snapname: "{{ proxmox_snap_name }}" + description: "Pre-firmware-update: {{ firmware_current_version }} → {{ firmware_new_version }}" -# # Snapshot BEFORE maintenance mode — the restore point should be -# # a fully functioning MASTER node, not one mid-failover -# - name: Take Proxmox VM snapshot -# community.general.proxmox_snap: -# api_host: "{{ proxmox_api_host }}" -# api_token_id: "{{ proxmox_api_token_id }}" -# api_token_secret: "{{ proxmox_api_token_secret }}" -# vmid: "{{ proxmox_vmid }}" -# state: present -# snapname: "{{ snap_name }}" -# description: "Pre-firmware-update: {{ firmware_current_version }} → {{ firmware_new_version }}" -# timeout: 60 -# delegate_to: localhost + - name: Enable CARP maintenance mode + ansible.builtin.uri: + url: "https://{{ opnsense_host }}/api/diagnostics/interface/carp_status/maintenance" + method: POST + user: "{{ opnsense_api_key }}" + password: "{{ opnsense_api_secret }}" + force_basic_auth: true + validate_certs: false + register: _enable_maintenance + changed_when: _enable_maintenance.status == "ok" -# - name: Enable CARP maintenance mode (triggers controlled failover to backup) -# ansible.builtin.uri: -# url: "https://{{ opnsense_host }}/api/carp/status/maintenancemode" -# method: POST -# user: "{{ opnsense_api_key }}" -# password: "{{ opnsense_api_secret }}" -# force_basic_auth: true -# validate_certs: false -# body_format: json -# body: {} + - name: Trigger firmware update + ansible.builtin.uri: + url: "https://{{ opnsense_host }}/api/core/firmware/update" + method: POST + user: "{{ opnsense_api_key }}" + password: "{{ opnsense_api_secret }}" + force_basic_auth: true + validate_certs: false -# - name: Wait for CARP failover to settle -# ansible.builtin.pause: -# seconds: 10 + - name: Wait for packages to update + ansible.builtin.uri: + url: "https://{{ opnsense_host }}/api/core/firmware/running" + method: GET + user: "{{ opnsense_api_key }}" + password: "{{ opnsense_api_secret }}" + force_basic_auth: true + validate_certs: false + register: _update_running + until: _update_running.json.status != 'busy' + retries: 300 + delay: 15 -# # Do not proceed if the backup has not actually taken over. -# # A failed failover means no traffic coverage during master update. -# - name: Confirm backup node is now MASTER -# ansible.builtin.uri: -# url: "https://{{ hostvars[groups['opnsense_backup'][0]].ansible_host }}/api/carp/status/get" -# method: GET -# user: "{{ opnsense_api_key }}" -# password: "{{ opnsense_api_secret }}" -# force_basic_auth: true -# validate_certs: false -# register: _backup_carp -# delegate_to: localhost -# failed_when: _backup_carp.json.status | upper != 'MASTER' + - name: Wait for node to go offline + ansible.builtin.wait_for: + host: "{{ ansible_host }}" + port: "{{ opnsense_https_port }}" + state: stopped + timeout: 60 + when: needs_reboot -# - name: Update and verify -# block: + - name: Wait for node to come back online + ansible.builtin.wait_for: + host: "{{ ansible_host }}" + port: "{{ opnsense_https_port }}" + state: started + timeout: 300 + delay: 30 + when: needs_reboot -# - name: Trigger firmware update -# ansible.builtin.uri: -# url: "https://{{ opnsense_host }}/api/core/firmware/update" -# method: POST -# user: "{{ opnsense_api_key }}" -# password: "{{ opnsense_api_secret }}" -# force_basic_auth: true -# validate_certs: false -# body_format: json -# body: {} + - name: Wait for API to be fully responsive + ansible.builtin.uri: + url: "https://{{ opnsense_host }}/api/core/firmware/status" + method: GET + user: "{{ opnsense_api_key }}" + password: "{{ opnsense_api_secret }}" + force_basic_auth: true + validate_certs: false + register: _post_status + until: _post_status.status == 200 + retries: 20 + delay: 10 -# - name: Wait for packages to install -# ansible.builtin.uri: -# url: "https://{{ opnsense_host }}/api/core/firmware/running" -# method: GET -# user: "{{ opnsense_api_key }}" -# password: "{{ opnsense_api_secret }}" -# force_basic_auth: true -# validate_certs: false -# register: _update_running -# until: _update_running.json.status != 'running' -# retries: 30 -# delay: 15 + - name: Check VIP status + ansible.builtin.uri: + url: "https://{{ opnsense_host }}/api/diagnostics/interface/get_vip_status" + method: GET + user: "{{ opnsense_api_key }}" + password: "{{ opnsense_api_secret }}" + force_basic_auth: true + validate_certs: false + register: _carp_post_udapte -# - name: Reboot node -# ansible.builtin.uri: -# url: "https://{{ opnsense_host }}/api/core/firmware/reboot" -# method: POST -# user: "{{ opnsense_api_key }}" -# password: "{{ opnsense_api_secret }}" -# force_basic_auth: true -# validate_certs: false -# body_format: json -# body: {} -# ignore_errors: true + - name: Disable CARP maintenance mode + ansible.builtin.uri: + url: "https://{{ opnsense_host }}/api/carp/status/maintenancemode" + method: POST + user: "{{ opnsense_api_key }}" + password: "{{ opnsense_api_secret }}" + force_basic_auth: true + validate_certs: false + when: _carp_post_udapte.json.carp.maintenancemode -# - name: Wait for node to go offline -# ansible.builtin.wait_for: -# host: "{{ ansible_host }}" -# port: 443 -# state: stopped -# timeout: 60 -# delegate_to: localhost + rescue: -# - name: Wait for node to come back online -# ansible.builtin.wait_for: -# host: "{{ ansible_host }}" -# port: 443 -# state: started -# timeout: 300 -# delay: 30 -# delegate_to: localhost + - name: Rollback VM to the pre-update snapshot + community.proxmox.proxmox_snap: + vmid: "{{ proxmox_vmid }}" + state: rollback + snapname: "{{ proxmox_snap_name }}" -# - name: Wait for API to be fully responsive -# ansible.builtin.uri: -# url: "https://{{ opnsense_host }}/api/core/firmware/status" -# method: GET -# user: "{{ opnsense_api_key }}" -# password: "{{ opnsense_api_secret }}" -# force_basic_auth: true -# validate_certs: false -# register: _post_status -# until: _post_status.status == 200 -# retries: 20 -# delay: 10 + - name: Send Ntfy notification + ansible.builtin.uri: + url: "{{ ntfy_url }}/{{ ntfy_topic }}" + method: POST + user: "{{ ntfy_user }}" + password: "{{ lookup('env', 'NTFY_PASSWORD') }}" + force_basic_auth: true + body: | + TODO + # Current version: {{ firmware_current_version }} + # {% if firmware_status != "update" %} + # {% if firmware_status == "upgrade" %} + # Upgrade to {{ firmware_upgrade_version }} available, relaunch the playbook manually to proceed. + # {% else %} + # No updates available on the cluster. + # {% endif %} + # {% elif in_maintenance %} + # The node {{ inventory_hostname }} is in CARP maintenance mode. + # {% elif mismatched_vips > 0 %} + # Some VIP are not {{ main_role }} on {{ ansible_host }}. + # {% else %} + # No VIPs are managed. + # {% endif %} + headers: + Title: "OPNsense update failed on {{ inventory_hostname }}" + Priority: "default" + Tags: "x" + run_once: true + when: ntfy_url is defined -# - name: Health check — no remaining updates -# ansible.builtin.assert: -# that: -# - _post_status.json.status == 'none' -# fail_msg: >- -# Post-update firmware status is '{{ _post_status.json.status }}'. -# Expected 'none'. Triggering rollback. - -# # After maintenance mode failover, master should rejoin as BACKUP -# - name: Health check — CARP role is BACKUP -# ansible.builtin.uri: -# url: "https://{{ opnsense_host }}/api/carp/status/get" -# method: GET -# user: "{{ opnsense_api_key }}" -# password: "{{ opnsense_api_secret }}" -# force_basic_auth: true -# validate_certs: false -# register: _carp_post -# failed_when: _carp_post.json.status | upper not in ['BACKUP', 'MAINTENANCE'] - -# - name: Record master update result -# ansible.builtin.set_fact: -# update_result: "success" -# fw_post_version: "{{ _post_status.json.product_version | default('unknown') }}" - -# rescue: -# # Snapshot was taken before maintenance mode, so the restored VM -# # will come back as a healthy MASTER on the previous version. -# # Backup (already updated) continues serving traffic during rollback. - -# - name: "ROLLBACK | Stop VM" -# ansible.builtin.uri: -# url: "{{ _pve_base }}/status/stop" -# method: POST -# headers: "{{ _pve_auth }}" -# validate_certs: false -# delegate_to: localhost - -# - name: "ROLLBACK | Wait for VM to stop" -# ansible.builtin.uri: -# url: "{{ _pve_base }}/status/current" -# method: GET -# headers: "{{ _pve_auth }}" -# validate_certs: false -# register: _vm_state -# until: _vm_state.json.data.status == 'stopped' -# retries: 12 -# delay: 5 -# delegate_to: localhost - -# - name: "ROLLBACK | Restore snapshot {{ snap_name }}" -# ansible.builtin.uri: -# url: "{{ _pve_base }}/snapshot/{{ snap_name }}/rollback" -# method: POST -# headers: "{{ _pve_auth }}" -# validate_certs: false -# delegate_to: localhost - -# # Proxmox rollback tasks run async; wait for completion -# - name: "ROLLBACK | Wait for snapshot restore to complete" -# ansible.builtin.pause: -# seconds: 30 - -# - name: "ROLLBACK | Start VM" -# ansible.builtin.uri: -# url: "{{ _pve_base }}/status/start" -# method: POST -# headers: "{{ _pve_auth }}" -# validate_certs: false -# delegate_to: localhost - -# - name: "ROLLBACK | Wait for VM to come back online" -# ansible.builtin.wait_for: -# host: "{{ ansible_host }}" -# port: 443 -# state: started -# timeout: 300 -# delay: 30 -# delegate_to: localhost - -# - name: "ROLLBACK | Record result" -# ansible.builtin.set_fact: -# update_result: "rolled_back" -# fw_post_version: "{{ firmware_current_version }}" - -# always: -# # Best-effort: disable maintenance mode whether update succeeded or rolled back. -# # On rollback, the restored snapshot predates maintenance mode so this -# # is effectively a no-op, but run it anyway as a safety net. -# - name: Disable CARP maintenance mode -# ansible.builtin.uri: -# url: "https://{{ opnsense_host }}/api/carp/status/maintenancemode" -# method: POST -# user: "{{ opnsense_api_key }}" -# password: "{{ opnsense_api_secret }}" -# force_basic_auth: true -# validate_certs: false -# body_format: json -# body: {} -# ignore_errors: true - - -# ############################################################### -# # Phase 4 targets localhost — it runs even if Phase 3 failed, -# # because localhost was never marked as a failed host. -# - name: "Phase 4 | Send ntfy notification" -# hosts: localhost -# gather_facts: false - -# tasks: -# - name: Build report -# ansible.builtin.set_fact: -# _report: | -# {% for host in groups['opnsense'] %} -# {% set h = hostvars[host] %} -# {{ host }} -# fw check : {{ h.firmware_status | default('unknown') }} -# result : {{ h.update_result | default('unknown') }} -# version : {{ h.fw_post_version | default(h.firmware_current_version | default('?')) }} -# {% endfor %} -# _any_issue: >- -# {{ groups['opnsense'] -# | map('extract', hostvars, 'update_result') -# | select('in', ['rolled_back', 'pending']) -# | list -# | length > 0 }} - -# - name: Send ntfy notification -# ansible.builtin.uri: -# url: "{{ ntfy_url }}/{{ ntfy_topic }}" -# method: POST -# headers: -# Authorization: "Bearer {{ ntfy_token }}" -# Title: "OPNsense HA Update Report" -# Priority: "{{ 'high' if _any_issue else 'default' }}" -# Tags: "{{ 'warning' if _any_issue else 'white_check_mark' }},shield" -# Content-Type: "text/plain" -# body: "{{ _report }}" - -# - name: Fail playbook if any node was rolled back or did not complete -# ansible.builtin.fail: -# msg: >- -# One or more nodes did not update successfully. -# Check ntfy report and Ansible output for details. -# when: _any_issue | bool \ No newline at end of file + - ansible.builtin.fail: + msg: "Update aborted" -- 2.54.0 From bbb2e0f02437d6c9e5e40d3f5389ff4c8a71d3b3 Mon Sep 17 00:00:00 2001 From: Vezpi Date: Sun, 21 Jun 2026 19:59:49 +0000 Subject: [PATCH 25/74] add: requirements.yml for community.proxmox --- ansible/opnsense/requirements.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 ansible/opnsense/requirements.yml diff --git a/ansible/opnsense/requirements.yml b/ansible/opnsense/requirements.yml new file mode 100644 index 0000000..77070df --- /dev/null +++ b/ansible/opnsense/requirements.yml @@ -0,0 +1,4 @@ +--- +collections: + - name: community.proxmox + version: "2.0.0" \ No newline at end of file -- 2.54.0 From a8b0fca2633dc88a7b4e274bbb9ee3ce487a03fc Mon Sep 17 00:00:00 2001 From: Vezpi Date: Sun, 21 Jun 2026 20:43:09 +0000 Subject: [PATCH 26/74] add: pause after CARP maintenace mode --- ansible/opnsense/update_opnsense_ha_cluster.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 2893ac0..57283c1 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -141,6 +141,10 @@ register: _enable_maintenance changed_when: _enable_maintenance.status == "ok" + - name: Wait for CARP failover to settle + ansible.builtin.pause: + seconds: 5 + - name: Trigger firmware update ansible.builtin.uri: url: "https://{{ opnsense_host }}/api/core/firmware/update" @@ -252,7 +256,7 @@ hosts: opnsense_master gather_facts: false vars: - proxmox_snap_name: "preupdate-{{ now().strftime('%Y%m%d-%H%M') }}" + proxmox_snap_name: "preupdate-{{ now().strftime('%Y%m%d') }}" tasks: -- 2.54.0 From e46f78b971d02c24866e2959c1fd0fd6519c27a8 Mon Sep 17 00:00:00 2001 From: Vezpi Date: Sun, 21 Jun 2026 20:44:41 +0000 Subject: [PATCH 27/74] fix: move the pause for the correct node --- ansible/opnsense/update_opnsense_ha_cluster.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 57283c1..6340098 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -141,10 +141,6 @@ register: _enable_maintenance changed_when: _enable_maintenance.status == "ok" - - name: Wait for CARP failover to settle - ansible.builtin.pause: - seconds: 5 - - name: Trigger firmware update ansible.builtin.uri: url: "https://{{ opnsense_host }}/api/core/firmware/update" @@ -280,6 +276,10 @@ register: _enable_maintenance changed_when: _enable_maintenance.status == "ok" + - name: Wait for CARP failover to settle + ansible.builtin.pause: + seconds: 5 + - name: Trigger firmware update ansible.builtin.uri: url: "https://{{ opnsense_host }}/api/core/firmware/update" -- 2.54.0 From 0a576b7ec0dec34829dbbdfe6ea39e59b271000d Mon Sep 17 00:00:00 2001 From: Vezpi Date: Sun, 21 Jun 2026 21:34:05 +0000 Subject: [PATCH 28/74] change: method to wait for the update --- .../opnsense/update_opnsense_ha_cluster.yml | 74 +++++++------------ 1 file changed, 28 insertions(+), 46 deletions(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 6340098..7cc39cd 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -114,7 +114,6 @@ headers: Title: "OPNsense update aborted" Priority: "default" - Tags: "x" run_once: true when: ntfy_url is defined @@ -223,30 +222,18 @@ password: "{{ lookup('env', 'NTFY_PASSWORD') }}" force_basic_auth: true body: | - TODO - # Current version: {{ firmware_current_version }} - # {% if firmware_status != "update" %} - # {% if firmware_status == "upgrade" %} - # Upgrade to {{ firmware_upgrade_version }} available, relaunch the playbook manually to proceed. - # {% else %} - # No updates available on the cluster. - # {% endif %} - # {% elif in_maintenance %} - # The node {{ inventory_hostname }} is in CARP maintenance mode. - # {% elif mismatched_vips > 0 %} - # Some VIP are not {{ main_role }} on {{ ansible_host }}. - # {% else %} - # No VIPs are managed. - # {% endif %} + Current version: {{ firmware_current_version }} + Update to {{ firmware_new_version }} failed on {{ inventory_hostname }} ({{ main_role }}). + No snapshot have been taken before the update. headers: Title: "OPNsense update failed on {{ inventory_hostname }}" - Priority: "default" + Priority: "high" Tags: "x" run_once: true when: ntfy_url is defined - ansible.builtin.fail: - msg: "Update aborted" + msg: "Update failed" - name: "Phase 3 | Update master node (Proxmox, with snapshot)" hosts: opnsense_master @@ -289,7 +276,7 @@ force_basic_auth: true validate_certs: false - - name: Wait for packages to update + - name: Check if the update is running ansible.builtin.uri: url: "https://{{ opnsense_host }}/api/core/firmware/running" method: GET @@ -298,16 +285,18 @@ force_basic_auth: true validate_certs: false register: _update_running - until: _update_running.json.status != 'busy' - retries: 300 - delay: 15 + + - name: Assert the update is running + ansible.builtin.assert: + that: _update_running.json.status == 'busy' + quiet: true - name: Wait for node to go offline ansible.builtin.wait_for: host: "{{ ansible_host }}" port: "{{ opnsense_https_port }}" state: stopped - timeout: 60 + timeout: 3600 when: needs_reboot - name: Wait for node to come back online @@ -319,18 +308,22 @@ delay: 30 when: needs_reboot - - name: Wait for API to be fully responsive + - name: Check firmware version ansible.builtin.uri: - url: "https://{{ opnsense_host }}/api/core/firmware/status" - method: GET + url: "https://{{ opnsense_host }}/api/core/firmware/check" + method: POST user: "{{ opnsense_api_key }}" password: "{{ opnsense_api_secret }}" force_basic_auth: true validate_certs: false - register: _post_status - until: _post_status.status == 200 - retries: 20 - delay: 10 + register: _post_firmware_check + until: _post_firmware_check.json.product_version | default('unknown') == firmware_new_version + retries: 300 + delay: 15 + + - name: Assert the node has been updated + ansible.builtin.assert: + that: - name: Check VIP status ansible.builtin.uri: @@ -359,6 +352,7 @@ vmid: "{{ proxmox_vmid }}" state: rollback snapname: "{{ proxmox_snap_name }}" + ignore_errors: true - name: Send Ntfy notification ansible.builtin.uri: @@ -368,24 +362,12 @@ password: "{{ lookup('env', 'NTFY_PASSWORD') }}" force_basic_auth: true body: | - TODO - # Current version: {{ firmware_current_version }} - # {% if firmware_status != "update" %} - # {% if firmware_status == "upgrade" %} - # Upgrade to {{ firmware_upgrade_version }} available, relaunch the playbook manually to proceed. - # {% else %} - # No updates available on the cluster. - # {% endif %} - # {% elif in_maintenance %} - # The node {{ inventory_hostname }} is in CARP maintenance mode. - # {% elif mismatched_vips > 0 %} - # Some VIP are not {{ main_role }} on {{ ansible_host }}. - # {% else %} - # No VIPs are managed. - # {% endif %} + Current version: {{ firmware_current_version }} + Update to {{ firmware_new_version }} failed on {{ inventory_hostname }} ({{ main_role }}). + A snapshot have been taken before the update and the VM has been rollback. headers: Title: "OPNsense update failed on {{ inventory_hostname }}" - Priority: "default" + Priority: "high" Tags: "x" run_once: true when: ntfy_url is defined -- 2.54.0 From ac69a7a5c65b279875b3150db9a0b91256d0169e Mon Sep 17 00:00:00 2001 From: Vezpi Date: Mon, 22 Jun 2026 06:53:02 +0000 Subject: [PATCH 29/74] fix: check firmware version --- ansible/opnsense/update_opnsense_ha_cluster.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 7cc39cd..ef70040 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -310,15 +310,15 @@ - name: Check firmware version ansible.builtin.uri: - url: "https://{{ opnsense_host }}/api/core/firmware/check" + url: "https://{{ opnsense_host }}/api/core/firmware/status" method: POST user: "{{ opnsense_api_key }}" password: "{{ opnsense_api_secret }}" force_basic_auth: true validate_certs: false - register: _post_firmware_check - until: _post_firmware_check.json.product_version | default('unknown') == firmware_new_version - retries: 300 + register: _post_firmware_status + until: _post_firmware_status.json.product_version | default('unknown') == firmware_new_version + retries: 240 delay: 15 - name: Assert the node has been updated -- 2.54.0 From b91ea088747d2c739d4522373d8dc0f684f9ad0c Mon Sep 17 00:00:00 2001 From: Vezpi Date: Mon, 22 Jun 2026 08:26:27 +0000 Subject: [PATCH 30/74] fix: remove useles assert --- ansible/opnsense/update_opnsense_ha_cluster.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index ef70040..a94ab7d 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -61,11 +61,11 @@ total_vips: "{{ _vip_status.json.rowCount }}" mismatched_vips: "{{ _vip_status.json.rows | rejectattr('status', 'equalto', main_role) | list | length }}" in_maintenance: "{{ _vip_status.json.carp.maintenancemode }}" - # update_result: "{{ 'pending' if _firmware_status.json.status == 'update' else 'skipped' }}" - name: Skip backup update ansible.builtin.set_fact: skip_update: true + firmware_status: "skipped" delegate_to: "{{ groups['opnsense_backup'][0] }}" delegate_facts: true run_once: true @@ -321,10 +321,6 @@ retries: 240 delay: 15 - - name: Assert the node has been updated - ansible.builtin.assert: - that: - - name: Check VIP status ansible.builtin.uri: url: "https://{{ opnsense_host }}/api/diagnostics/interface/get_vip_status" -- 2.54.0 From 4c086cfad5cfdc3638a35a9b8655ec46eae279e6 Mon Sep 17 00:00:00 2001 From: Vezpi Date: Mon, 22 Jun 2026 10:01:34 +0000 Subject: [PATCH 31/74] test: define proxmox token ID in the task --- ansible/opnsense/update_opnsense_ha_cluster.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index a94ab7d..6db0d4d 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -247,6 +247,8 @@ - name: Take Proxmox VM snapshot community.general.proxmox_snap: + api_token_id: "{{ lookup('env', 'PROXMOX_TOKEN_ID') }}" + api_token_secret: "{{ lookup('env', 'PROXMOX_TOKEN_SECRET') }}" vmid: "{{ proxmox_vmid }}" state: present snapname: "{{ proxmox_snap_name }}" @@ -348,6 +350,7 @@ vmid: "{{ proxmox_vmid }}" state: rollback snapname: "{{ proxmox_snap_name }}" + register: _proxmox_snapshot_rollback ignore_errors: true - name: Send Ntfy notification @@ -360,6 +363,7 @@ body: | Current version: {{ firmware_current_version }} Update to {{ firmware_new_version }} failed on {{ inventory_hostname }} ({{ main_role }}). + {% if } A snapshot have been taken before the update and the VM has been rollback. headers: Title: "OPNsense update failed on {{ inventory_hostname }}" -- 2.54.0 From 1ca4807d4b8191514ca845df8816220cdf253078 Mon Sep 17 00:00:00 2001 From: Vezpi Date: Mon, 22 Jun 2026 10:10:33 +0000 Subject: [PATCH 32/74] fix: wip on notifications --- ansible/opnsense/update_opnsense_ha_cluster.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 6db0d4d..0b30332 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -363,7 +363,7 @@ body: | Current version: {{ firmware_current_version }} Update to {{ firmware_new_version }} failed on {{ inventory_hostname }} ({{ main_role }}). - {% if } + # {% if } A snapshot have been taken before the update and the VM has been rollback. headers: Title: "OPNsense update failed on {{ inventory_hostname }}" -- 2.54.0 From 79302e8139e570202abcf714ca59b7f120de24e6 Mon Sep 17 00:00:00 2001 From: Vezpi Date: Mon, 22 Jun 2026 19:29:39 +0000 Subject: [PATCH 33/74] test: add api_user for proxmox snapshot --- ansible/opnsense/update_opnsense_ha_cluster.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 0b30332..907b201 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -247,6 +247,7 @@ - name: Take Proxmox VM snapshot community.general.proxmox_snap: + api_user: "{{ lookup('env', 'PROXMOX_USER') }}" api_token_id: "{{ lookup('env', 'PROXMOX_TOKEN_ID') }}" api_token_secret: "{{ lookup('env', 'PROXMOX_TOKEN_SECRET') }}" vmid: "{{ proxmox_vmid }}" -- 2.54.0 From fd0a9a83e88b2e21fad841d0936384345abe7815 Mon Sep 17 00:00:00 2001 From: Vezpi Date: Mon, 22 Jun 2026 19:35:46 +0000 Subject: [PATCH 34/74] change: use proxmox collection --- ansible/opnsense/update_opnsense_ha_cluster.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 907b201..5256fc6 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -246,10 +246,7 @@ - block: - name: Take Proxmox VM snapshot - community.general.proxmox_snap: - api_user: "{{ lookup('env', 'PROXMOX_USER') }}" - api_token_id: "{{ lookup('env', 'PROXMOX_TOKEN_ID') }}" - api_token_secret: "{{ lookup('env', 'PROXMOX_TOKEN_SECRET') }}" + community.proxmox.proxmox_snap: vmid: "{{ proxmox_vmid }}" state: present snapname: "{{ proxmox_snap_name }}" -- 2.54.0 From 22de950cf57e8084d7e643bd841febbd25d9aa1a Mon Sep 17 00:00:00 2001 From: Vezpi Date: Mon, 22 Jun 2026 20:14:01 +0000 Subject: [PATCH 35/74] refactor: merge assert with the task before --- ansible/opnsense/update_opnsense_ha_cluster.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 5256fc6..5464dc3 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -285,13 +285,9 @@ force_basic_auth: true validate_certs: false register: _update_running + failedd_when: _update_running.json.status != 'busy' - - name: Assert the update is running - ansible.builtin.assert: - that: _update_running.json.status == 'busy' - quiet: true - - - name: Wait for node to go offline + - name: Wait for node to reboot after the update ansible.builtin.wait_for: host: "{{ ansible_host }}" port: "{{ opnsense_https_port }}" -- 2.54.0 From b1008ec42566d7f96f72bb5bdb34a2a5edfe2346 Mon Sep 17 00:00:00 2001 From: Vezpi Date: Tue, 23 Jun 2026 19:15:03 +0000 Subject: [PATCH 36/74] add: notification on success --- .../opnsense/update_opnsense_ha_cluster.yml | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 5464dc3..8a79acf 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -329,7 +329,7 @@ - name: Disable CARP maintenance mode ansible.builtin.uri: - url: "https://{{ opnsense_host }}/api/carp/status/maintenancemode" + url: "https://{{ opnsense_host }}/api/diagnostics/interface/carp_status/maintenance" method: POST user: "{{ opnsense_api_key }}" password: "{{ opnsense_api_secret }}" @@ -368,3 +368,31 @@ - ansible.builtin.fail: msg: "Update aborted" + + +- name: "Phase 4 | Notify on succes" + hosts: localhost + gather_facts: false + tasks: + + - name: Send Ntfy notification + vars: + master: "{{ groups['opnsense_master'][0] }}" + backup: "{{ groups['opnsense_backup'][0] }}" + ansible.builtin.uri: + url: "{{ ntfy_url }}/{{ ntfy_topic }}" + method: POST + user: "{{ ntfy_user }}" + password: "{{ lookup('env', 'NTFY_PASSWORD') }}" + force_basic_auth: true + body: | + OPNsense HA cluster update completed successfully. + {% for host in [master, backup] %} + {% set h = hostvars[host] %} + {{ h.main_role }} ({{ host }}): {% if h.skip_update %}already on {{ h.firmware_current_version }}, skipped{% else %}{{ h.firmware_current_version }} → {{ h.firmware_new_version }}{% endif %}{{ "\n" if not loop.last else "" }} + {% endfor %} + headers: + Title: "OPNsense update completed" + Priority: "default" + Tags: "white_check_mark" + when: ntfy_url is defined \ No newline at end of file -- 2.54.0 From 137b13d94e8ecb023d5eb37d87a9d8cab76173af Mon Sep 17 00:00:00 2001 From: Vezpi Date: Tue, 23 Jun 2026 19:15:50 +0000 Subject: [PATCH 37/74] fix: typo --- ansible/opnsense/update_opnsense_ha_cluster.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 8a79acf..3a94782 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -285,7 +285,7 @@ force_basic_auth: true validate_certs: false register: _update_running - failedd_when: _update_running.json.status != 'busy' + failed_when: _update_running.json.status != 'busy' - name: Wait for node to reboot after the update ansible.builtin.wait_for: -- 2.54.0 From 5d39cf31720f2012de38f9e47228c723e5b0313d Mon Sep 17 00:00:00 2001 From: Vezpi Date: Tue, 23 Jun 2026 19:26:22 +0000 Subject: [PATCH 38/74] change: align phase 2 with phase 3 --- .../opnsense/update_opnsense_ha_cluster.yml | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 3a94782..497119b 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -149,7 +149,7 @@ force_basic_auth: true validate_certs: false - - name: Wait for packages to update + - name: Check if the update is running ansible.builtin.uri: url: "https://{{ opnsense_host }}/api/core/firmware/running" method: GET @@ -158,16 +158,14 @@ force_basic_auth: true validate_certs: false register: _update_running - until: _update_running.json.status != 'busy' - retries: 300 - delay: 15 + failed_when: _update_running.json.status != 'busy' - - name: Wait for node to go offline + - name: Wait for node to reboot after the update ansible.builtin.wait_for: host: "{{ ansible_host }}" port: "{{ opnsense_https_port }}" state: stopped - timeout: 60 + timeout: 3600 when: needs_reboot - name: Wait for node to come back online @@ -179,19 +177,19 @@ delay: 30 when: needs_reboot - - name: Wait for API to be fully responsive + - name: Check firmware version ansible.builtin.uri: url: "https://{{ opnsense_host }}/api/core/firmware/status" - method: GET + method: POST user: "{{ opnsense_api_key }}" password: "{{ opnsense_api_secret }}" force_basic_auth: true validate_certs: false - register: _post_status - until: _post_status.status == 200 - retries: 20 - delay: 10 - + register: _post_firmware_status + until: _post_firmware_status.json.product_version | default('unknown') == firmware_new_version + retries: 240 + delay: 15 + - name: Check VIP status ansible.builtin.uri: url: "https://{{ opnsense_host }}/api/diagnostics/interface/get_vip_status" -- 2.54.0 From 869faf720ae0deea1c76703f97c720d79c0f5b21 Mon Sep 17 00:00:00 2001 From: Vezpi Date: Tue, 23 Jun 2026 19:27:26 +0000 Subject: [PATCH 39/74] add: any_errors_fatal for phase 2 and 3 --- ansible/opnsense/update_opnsense_ha_cluster.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 497119b..e7d5429 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -123,6 +123,7 @@ - name: "Phase 2 | Update backup node (TrueNAS, no snapshot)" hosts: opnsense_backup + any_errors_fatal: true gather_facts: false tasks: @@ -189,7 +190,7 @@ until: _post_firmware_status.json.product_version | default('unknown') == firmware_new_version retries: 240 delay: 15 - + - name: Check VIP status ansible.builtin.uri: url: "https://{{ opnsense_host }}/api/diagnostics/interface/get_vip_status" @@ -235,6 +236,7 @@ - name: "Phase 3 | Update master node (Proxmox, with snapshot)" hosts: opnsense_master + any_errors_fatal: true gather_facts: false vars: proxmox_snap_name: "preupdate-{{ now().strftime('%Y%m%d') }}" -- 2.54.0 From cbcbc45d004f2eb5922eef981bb8585e5bd74d0b Mon Sep 17 00:00:00 2001 From: Vezpi Date: Tue, 23 Jun 2026 20:50:49 +0000 Subject: [PATCH 40/74] add: workaround for setting maintenance mode after the update --- ansible/opnsense/update_opnsense_ha_cluster.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index e7d5429..ee6a7b6 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -336,6 +336,7 @@ force_basic_auth: true validate_certs: false when: _carp_post_udapte.json.carp.maintenancemode + ignore_errors: true rescue: -- 2.54.0 From f78c8a6cd7fba5ad5d15b7f1eb08d194dd1cf934 Mon Sep 17 00:00:00 2001 From: Vezpi Date: Tue, 23 Jun 2026 20:51:21 +0000 Subject: [PATCH 41/74] add: workaround for phase 2 also --- ansible/opnsense/update_opnsense_ha_cluster.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index ee6a7b6..68b9957 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -210,6 +210,7 @@ force_basic_auth: true validate_certs: false when: _carp_post_udapte.json.carp.maintenancemode + ignore_errors: true rescue: -- 2.54.0 From f0ded8ac17f902962c0216c618ea775f63cb30c2 Mon Sep 17 00:00:00 2001 From: Vezpi Date: Tue, 23 Jun 2026 21:39:34 +0000 Subject: [PATCH 42/74] fix: assertion on skip_update undefined --- ansible/opnsense/update_opnsense_ha_cluster.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 68b9957..f6cf1b4 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -74,7 +74,7 @@ - name: "Assert" ansible.builtin.assert: that: - - firmware_status == 'update' or skip_update + - firmware_status == 'update' or skip_update | default(false) - not in_maintenance - mismatched_vips == 0 - total_vips > 0 -- 2.54.0 From 43e535143b2bfd34247cb51cbf28886a5ca688bc Mon Sep 17 00:00:00 2001 From: Vezpi Date: Thu, 25 Jun 2026 21:09:35 +0000 Subject: [PATCH 43/74] add: firmware action --- .../opnsense/update_opnsense_ha_cluster.yml | 45 ++++++++++--------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index f6cf1b4..af2327c 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -53,6 +53,7 @@ - name: Store firmware facts ansible.builtin.set_fact: + firmware_action: "{{ opnsense_action | default('update')}}" firmware_status: "{{ _firmware_status.json.status }}" firmware_current_version: "{{ _firmware_status.json.product_version | default('unknown') }}" firmware_new_version: "{{ _firmware_status.json.upgrade_packages | selectattr('name', 'equalto', 'opnsense') | map(attribute='new_version') | first | default('') }}" @@ -62,7 +63,7 @@ mismatched_vips: "{{ _vip_status.json.rows | rejectattr('status', 'equalto', main_role) | list | length }}" in_maintenance: "{{ _vip_status.json.carp.maintenancemode }}" - - name: Skip backup update + - name: Skip backup {{ firmware_action }} ansible.builtin.set_fact: skip_update: true firmware_status: "skipped" @@ -74,7 +75,7 @@ - name: "Assert" ansible.builtin.assert: that: - - firmware_status == 'update' or skip_update | default(false) + - firmware_action == firmware_status or skip_update | default(false) - not in_maintenance - mismatched_vips == 0 - total_vips > 0 @@ -98,7 +99,7 @@ force_basic_auth: true body: | Current version: {{ firmware_current_version }} - {% if firmware_status != "update" %} + {% if firmware_action != firmware_status %} {% if firmware_status == "upgrade" %} Upgrade to {{ firmware_upgrade_version }} available, relaunch the playbook manually to proceed. {% else %} @@ -112,7 +113,7 @@ No VIPs are managed. {% endif %} headers: - Title: "OPNsense update aborted" + Title: "OPNsense {{ firmware_action }} aborted" Priority: "default" run_once: true when: ntfy_url is defined @@ -141,16 +142,16 @@ register: _enable_maintenance changed_when: _enable_maintenance.status == "ok" - - name: Trigger firmware update + - name: Trigger firmware {{ firmware_action }} ansible.builtin.uri: - url: "https://{{ opnsense_host }}/api/core/firmware/update" + url: "https://{{ opnsense_host }}/api/core/firmware/{{ firmware_action }}" method: POST user: "{{ opnsense_api_key }}" password: "{{ opnsense_api_secret }}" force_basic_auth: true validate_certs: false - - name: Check if the update is running + - name: Check if the {{ firmware_action }} is running ansible.builtin.uri: url: "https://{{ opnsense_host }}/api/core/firmware/running" method: GET @@ -161,7 +162,7 @@ register: _update_running failed_when: _update_running.json.status != 'busy' - - name: Wait for node to reboot after the update + - name: Wait for node to reboot after the {{ firmware_action }} ansible.builtin.wait_for: host: "{{ ansible_host }}" port: "{{ opnsense_https_port }}" @@ -223,10 +224,10 @@ force_basic_auth: true body: | Current version: {{ firmware_current_version }} - Update to {{ firmware_new_version }} failed on {{ inventory_hostname }} ({{ main_role }}). - No snapshot have been taken before the update. + {{ firmware_action | capitalize }} to {{ firmware_new_version }} failed on {{ inventory_hostname }} ({{ main_role }}). + No snapshot have been taken before the {{ firmware_action }}. headers: - Title: "OPNsense update failed on {{ inventory_hostname }}" + Title: "OPNsense {{ firmware_action }} failed on {{ inventory_hostname }}" Priority: "high" Tags: "x" run_once: true @@ -251,7 +252,7 @@ vmid: "{{ proxmox_vmid }}" state: present snapname: "{{ proxmox_snap_name }}" - description: "Pre-firmware-update: {{ firmware_current_version }} → {{ firmware_new_version }}" + description: "Pre-firmware-{{ firmware_action }}: {{ firmware_current_version }} → {{ firmware_new_version }}" - name: Enable CARP maintenance mode ansible.builtin.uri: @@ -268,16 +269,16 @@ ansible.builtin.pause: seconds: 5 - - name: Trigger firmware update + - name: Trigger firmware {{ firmware_action }} ansible.builtin.uri: - url: "https://{{ opnsense_host }}/api/core/firmware/update" + url: "https://{{ opnsense_host }}/api/core/firmware/{{ firmware_action }}" method: POST user: "{{ opnsense_api_key }}" password: "{{ opnsense_api_secret }}" force_basic_auth: true validate_certs: false - - name: Check if the update is running + - name: Check if the {{ firmware_action }} is running ansible.builtin.uri: url: "https://{{ opnsense_host }}/api/core/firmware/running" method: GET @@ -288,7 +289,7 @@ register: _update_running failed_when: _update_running.json.status != 'busy' - - name: Wait for node to reboot after the update + - name: Wait for node to reboot after the {{ firmware_action }} ansible.builtin.wait_for: host: "{{ ansible_host }}" port: "{{ opnsense_https_port }}" @@ -341,7 +342,7 @@ rescue: - - name: Rollback VM to the pre-update snapshot + - name: Rollback VM to the pre-{{ firmware_action }} snapshot community.proxmox.proxmox_snap: vmid: "{{ proxmox_vmid }}" state: rollback @@ -358,11 +359,11 @@ force_basic_auth: true body: | Current version: {{ firmware_current_version }} - Update to {{ firmware_new_version }} failed on {{ inventory_hostname }} ({{ main_role }}). + {{ firmware_action | capitalize }} to {{ firmware_new_version }} failed on {{ inventory_hostname }} ({{ main_role }}). # {% if } - A snapshot have been taken before the update and the VM has been rollback. + A snapshot have been taken before the {{ firmware_action }} and the VM has been rollback. headers: - Title: "OPNsense update failed on {{ inventory_hostname }}" + Title: "OPNsense {{ firmware_action }} failed on {{ inventory_hostname }}" Priority: "high" Tags: "x" run_once: true @@ -388,13 +389,13 @@ password: "{{ lookup('env', 'NTFY_PASSWORD') }}" force_basic_auth: true body: | - OPNsense HA cluster update completed successfully. + OPNsense HA cluster {{ firmware_action }} completed successfully. {% for host in [master, backup] %} {% set h = hostvars[host] %} {{ h.main_role }} ({{ host }}): {% if h.skip_update %}already on {{ h.firmware_current_version }}, skipped{% else %}{{ h.firmware_current_version }} → {{ h.firmware_new_version }}{% endif %}{{ "\n" if not loop.last else "" }} {% endfor %} headers: - Title: "OPNsense update completed" + Title: "OPNsense {{ firmware_action }} completed" Priority: "default" Tags: "white_check_mark" when: ntfy_url is defined \ No newline at end of file -- 2.54.0 From fa334f90d4c7441540cbf0257408e1b2b3ecb4d1 Mon Sep 17 00:00:00 2001 From: Vezpi Date: Fri, 26 Jun 2026 07:12:32 +0000 Subject: [PATCH 44/74] add: firmware upgrade message --- .../opnsense/update_opnsense_ha_cluster.yml | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index af2327c..555335a 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -51,13 +51,14 @@ retries: 12 delay: 5 - - name: Store firmware facts + - name: Store node facts ansible.builtin.set_fact: firmware_action: "{{ opnsense_action | default('update')}}" firmware_status: "{{ _firmware_status.json.status }}" firmware_current_version: "{{ _firmware_status.json.product_version | default('unknown') }}" firmware_new_version: "{{ _firmware_status.json.upgrade_packages | selectattr('name', 'equalto', 'opnsense') | map(attribute='new_version') | first | default('') }}" firmware_upgrade_version: "{{ _firmware_status.json.upgrade_major_version | default('unknown') }}" + firmware_upgrade_message: "{{ _firmware_status.json.upgrade_major_message | default('') }}" needs_reboot: "{{ true if _firmware_status.json.needs_reboot == '1' else false }}" total_vips: "{{ _vip_status.json.rowCount }}" mismatched_vips: "{{ _vip_status.json.rows | rejectattr('status', 'equalto', main_role) | list | length }}" @@ -81,13 +82,6 @@ - total_vips > 0 quiet: true - - name: Display firmware status - ansible.builtin.debug: - msg: >- - {{ inventory_hostname }} ({{ main_role }}): {{ firmware_status }} - ({{ firmware_current_version }} - {% if firmware_new_version %} → {{ firmware_new_version }} (reboot: {{ needs_reboot }}){% endif %}) - rescue: - name: Send Ntfy notification @@ -100,11 +94,15 @@ body: | Current version: {{ firmware_current_version }} {% if firmware_action != firmware_status %} - {% if firmware_status == "upgrade" %} - Upgrade to {{ firmware_upgrade_version }} available, relaunch the playbook manually to proceed. - {% else %} + {% if firmware_status == "upgrade" %} + Upgrade to {{ firmware_upgrade_version }} available, but no upgrade requested. + {% if firmware_upgrade_message != "" %} + Upgrade message: + {{ firmware_upgrade_message }} + {% endif %} + {% else %} No updates available on the cluster. - {% endif %} + {% endif %} {% elif in_maintenance %} The node {{ inventory_hostname }} is in CARP maintenance mode. {% elif mismatched_vips > 0 %} -- 2.54.0 From 3f2b208651e0566f869d037965b073dad62a9a79 Mon Sep 17 00:00:00 2001 From: Vezpi Date: Fri, 26 Jun 2026 07:13:29 +0000 Subject: [PATCH 45/74] fix: notification title --- ansible/opnsense/update_opnsense_ha_cluster.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 555335a..d125085 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -111,7 +111,7 @@ No VIPs are managed. {% endif %} headers: - Title: "OPNsense {{ firmware_action }} aborted" + Title: "OPNsense {{ firmware_action }} report" Priority: "default" run_once: true when: ntfy_url is defined -- 2.54.0 From 4a7e8ef7ca87836b91adee8f07ce5006e82d9744 Mon Sep 17 00:00:00 2001 From: Vezpi Date: Fri, 26 Jun 2026 19:54:49 +0000 Subject: [PATCH 46/74] add: message rewrite using filters --- ansible/opnsense/update_opnsense_ha_cluster.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index d125085..47f6b87 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -58,7 +58,7 @@ firmware_current_version: "{{ _firmware_status.json.product_version | default('unknown') }}" firmware_new_version: "{{ _firmware_status.json.upgrade_packages | selectattr('name', 'equalto', 'opnsense') | map(attribute='new_version') | first | default('') }}" firmware_upgrade_version: "{{ _firmware_status.json.upgrade_major_version | default('unknown') }}" - firmware_upgrade_message: "{{ _firmware_status.json.upgrade_major_message | default('') }}" + firmware_upgrade_message: "{{ _firmware_status.json.upgrade_major_message | regex_replace('<[^>]+>', ' ') | regex_replace('\s{2,}', ' ') | trim | default('') }}" needs_reboot: "{{ true if _firmware_status.json.needs_reboot == '1' else false }}" total_vips: "{{ _vip_status.json.rowCount }}" mismatched_vips: "{{ _vip_status.json.rows | rejectattr('status', 'equalto', main_role) | list | length }}" -- 2.54.0 From ff19c2faae5f47937ea1b2f16cd9a074cf062245 Mon Sep 17 00:00:00 2001 From: Vezpi Date: Fri, 26 Jun 2026 19:56:01 +0000 Subject: [PATCH 47/74] change: retries for firmware check --- ansible/opnsense/update_opnsense_ha_cluster.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 47f6b87..849f400 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -48,8 +48,8 @@ validate_certs: false register: _firmware_status until: _firmware_status.json.status != 'none' - retries: 12 - delay: 5 + retries: 10 + delay: 2 - name: Store node facts ansible.builtin.set_fact: -- 2.54.0 From d5c36e3118be5e323653bbf14873bea3cf35a355 Mon Sep 17 00:00:00 2001 From: Vezpi Date: Fri, 26 Jun 2026 19:58:26 +0000 Subject: [PATCH 48/74] change: replace fail by end_host --- ansible/opnsense/update_opnsense_ha_cluster.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 849f400..a056ba6 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -116,8 +116,8 @@ run_once: true when: ntfy_url is defined - - ansible.builtin.fail: - msg: "Update aborted" + - name: Skip + ansible.builtin.meta: end_host - name: "Phase 2 | Update backup node (TrueNAS, no snapshot)" -- 2.54.0 From 2d80a76353e0815d0ba89b07d4b206ef75b51f64 Mon Sep 17 00:00:00 2001 From: Vezpi Date: Fri, 26 Jun 2026 20:08:16 +0000 Subject: [PATCH 49/74] fix: backslash --- ansible/opnsense/update_opnsense_ha_cluster.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index a056ba6..1b9a94f 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -58,7 +58,7 @@ firmware_current_version: "{{ _firmware_status.json.product_version | default('unknown') }}" firmware_new_version: "{{ _firmware_status.json.upgrade_packages | selectattr('name', 'equalto', 'opnsense') | map(attribute='new_version') | first | default('') }}" firmware_upgrade_version: "{{ _firmware_status.json.upgrade_major_version | default('unknown') }}" - firmware_upgrade_message: "{{ _firmware_status.json.upgrade_major_message | regex_replace('<[^>]+>', ' ') | regex_replace('\s{2,}', ' ') | trim | default('') }}" + firmware_upgrade_message: "{{ _firmware_status.json.upgrade_major_message | regex_replace('<[^>]+>', ' ') | regex_replace('\\s{2,}', ' ') | trim | default('') }}" needs_reboot: "{{ true if _firmware_status.json.needs_reboot == '1' else false }}" total_vips: "{{ _vip_status.json.rowCount }}" mismatched_vips: "{{ _vip_status.json.rows | rejectattr('status', 'equalto', main_role) | list | length }}" -- 2.54.0 From fac4a27b75691f67067cfa1904e90d0c246be3e8 Mon Sep 17 00:00:00 2001 From: Vezpi Date: Fri, 26 Jun 2026 20:19:07 +0000 Subject: [PATCH 50/74] fix: use end_play instead of end_host --- ansible/opnsense/update_opnsense_ha_cluster.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 1b9a94f..24c8b07 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -117,7 +117,7 @@ when: ntfy_url is defined - name: Skip - ansible.builtin.meta: end_host + ansible.builtin.meta: end_play - name: "Phase 2 | Update backup node (TrueNAS, no snapshot)" -- 2.54.0 From 557f80979b1b6abb70a2b6a1c0c16bf3bcdc66cf Mon Sep 17 00:00:00 2001 From: Vezpi Date: Fri, 26 Jun 2026 20:49:46 +0000 Subject: [PATCH 51/74] revert: use fail instead of end_play --- ansible/opnsense/update_opnsense_ha_cluster.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 24c8b07..e242b04 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -116,8 +116,8 @@ run_once: true when: ntfy_url is defined - - name: Skip - ansible.builtin.meta: end_play + - ansible.builtin.fail: + msg: "Update aborted" - name: "Phase 2 | Update backup node (TrueNAS, no snapshot)" -- 2.54.0 From 2404692a30b9f996cead3778f8d41f26c917ff9e Mon Sep 17 00:00:00 2001 From: Vezpi Date: Sat, 27 Jun 2026 20:45:47 +0000 Subject: [PATCH 52/74] add: checks between update and upgrade --- .../opnsense/update_opnsense_ha_cluster.yml | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index e242b04..4503906 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -55,11 +55,14 @@ ansible.builtin.set_fact: firmware_action: "{{ opnsense_action | default('update')}}" firmware_status: "{{ _firmware_status.json.status }}" - firmware_current_version: "{{ _firmware_status.json.product_version | default('unknown') }}" + firmware_current_version: "{{ _firmware_status.json.product.product_version | default('unknown') }}" + firmware_product_series: "{{ _firmware_status.json.product.product_series | default('unknown') }}" firmware_new_version: "{{ _firmware_status.json.upgrade_packages | selectattr('name', 'equalto', 'opnsense') | map(attribute='new_version') | first | default('') }}" firmware_upgrade_version: "{{ _firmware_status.json.upgrade_major_version | default('unknown') }}" firmware_upgrade_message: "{{ _firmware_status.json.upgrade_major_message | regex_replace('<[^>]+>', ' ') | regex_replace('\\s{2,}', ' ') | trim | default('') }}" - needs_reboot: "{{ true if _firmware_status.json.needs_reboot == '1' else false }}" + firmware_target_kind: "{{ 'series' if firmware_status == 'upgrade' else 'version' }}" + firmware_target_value: "{{ firmware_upgrade_version if firmware_status == 'upgrade' else firmware_new_version }}" + needs_reboot: "{{ (_firmware_status.json.upgrade_needs_reboot if opnsense_action == 'upgrade' else _firmware_status.json.needs_reboot) == '1' }}" total_vips: "{{ _vip_status.json.rowCount }}" mismatched_vips: "{{ _vip_status.json.rows | rejectattr('status', 'equalto', main_role) | list | length }}" in_maintenance: "{{ _vip_status.json.carp.maintenancemode }}" @@ -72,7 +75,12 @@ delegate_facts: true run_once: true when: hostvars[groups['opnsense_backup'][0]].firmware_current_version == hostvars[groups['opnsense_master'][0]].firmware_new_version - + when: >- + (hostvars[groups['opnsense_backup'][0]] + ['firmware_current_version' + if hostvars[groups['opnsense_master'][0]].firmware_target_kind == 'version' + else 'firmware_product_series']) + == hostvars[groups['opnsense_master'][0]].firmware_target_value - name: "Assert" ansible.builtin.assert: that: @@ -186,7 +194,7 @@ force_basic_auth: true validate_certs: false register: _post_firmware_status - until: _post_firmware_status.json.product_version | default('unknown') == firmware_new_version + until: _post_firmware_status.json['product_' ~ firmware_target_kind] | default('unknown') == firmware_target_value retries: 240 delay: 15 @@ -313,7 +321,7 @@ force_basic_auth: true validate_certs: false register: _post_firmware_status - until: _post_firmware_status.json.product_version | default('unknown') == firmware_new_version + until: _post_firmware_status.json['product_' ~ firmware_target_kind] | default('unknown') == firmware_target_value retries: 240 delay: 15 -- 2.54.0 From 3d18c01d5af382aae847e1a89ba26a83052f14ca Mon Sep 17 00:00:00 2001 From: Vezpi Date: Sat, 27 Jun 2026 20:50:01 +0000 Subject: [PATCH 53/74] fix: undefined variable --- ansible/opnsense/update_opnsense_ha_cluster.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 4503906..6b7f277 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -60,8 +60,8 @@ firmware_new_version: "{{ _firmware_status.json.upgrade_packages | selectattr('name', 'equalto', 'opnsense') | map(attribute='new_version') | first | default('') }}" firmware_upgrade_version: "{{ _firmware_status.json.upgrade_major_version | default('unknown') }}" firmware_upgrade_message: "{{ _firmware_status.json.upgrade_major_message | regex_replace('<[^>]+>', ' ') | regex_replace('\\s{2,}', ' ') | trim | default('') }}" - firmware_target_kind: "{{ 'series' if firmware_status == 'upgrade' else 'version' }}" - firmware_target_value: "{{ firmware_upgrade_version if firmware_status == 'upgrade' else firmware_new_version }}" + firmware_target_kind: "{{ 'series' if _firmware_status.json.status == 'upgrade' else 'version' }}" + firmware_target_value: "{{ firmware_upgrade_version if _firmware_status.json.status == 'upgrade' else firmware_new_version }}" needs_reboot: "{{ (_firmware_status.json.upgrade_needs_reboot if opnsense_action == 'upgrade' else _firmware_status.json.needs_reboot) == '1' }}" total_vips: "{{ _vip_status.json.rowCount }}" mismatched_vips: "{{ _vip_status.json.rows | rejectattr('status', 'equalto', main_role) | list | length }}" @@ -74,13 +74,13 @@ delegate_to: "{{ groups['opnsense_backup'][0] }}" delegate_facts: true run_once: true - when: hostvars[groups['opnsense_backup'][0]].firmware_current_version == hostvars[groups['opnsense_master'][0]].firmware_new_version when: >- (hostvars[groups['opnsense_backup'][0]] ['firmware_current_version' if hostvars[groups['opnsense_master'][0]].firmware_target_kind == 'version' else 'firmware_product_series']) == hostvars[groups['opnsense_master'][0]].firmware_target_value + - name: "Assert" ansible.builtin.assert: that: -- 2.54.0 From 8ab282eb95d8718652539000e8927028cc8a5999 Mon Sep 17 00:00:00 2001 From: Vezpi Date: Sat, 27 Jun 2026 20:59:43 +0000 Subject: [PATCH 54/74] add: 2nd set_fact for readability --- .../opnsense/update_opnsense_ha_cluster.yml | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 6b7f277..29b9d2e 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -57,16 +57,21 @@ firmware_status: "{{ _firmware_status.json.status }}" firmware_current_version: "{{ _firmware_status.json.product.product_version | default('unknown') }}" firmware_product_series: "{{ _firmware_status.json.product.product_series | default('unknown') }}" - firmware_new_version: "{{ _firmware_status.json.upgrade_packages | selectattr('name', 'equalto', 'opnsense') | map(attribute='new_version') | first | default('') }}" + firmware_update_version: "{{ _firmware_status.json.upgrade_packages | selectattr('name', 'equalto', 'opnsense') | map(attribute='new_version') | first | default('') }}" firmware_upgrade_version: "{{ _firmware_status.json.upgrade_major_version | default('unknown') }}" firmware_upgrade_message: "{{ _firmware_status.json.upgrade_major_message | regex_replace('<[^>]+>', ' ') | regex_replace('\\s{2,}', ' ') | trim | default('') }}" - firmware_target_kind: "{{ 'series' if _firmware_status.json.status == 'upgrade' else 'version' }}" - firmware_target_value: "{{ firmware_upgrade_version if _firmware_status.json.status == 'upgrade' else firmware_new_version }}" - needs_reboot: "{{ (_firmware_status.json.upgrade_needs_reboot if opnsense_action == 'upgrade' else _firmware_status.json.needs_reboot) == '1' }}" + update_needs_reboot: "{{ _firmware_status.json.needs_reboot == '1' }}" + upgrade_needs_reboot: "{{ _firmware_status.json.upgrade_needs_reboot == '1' }}" total_vips: "{{ _vip_status.json.rowCount }}" mismatched_vips: "{{ _vip_status.json.rows | rejectattr('status', 'equalto', main_role) | list | length }}" in_maintenance: "{{ _vip_status.json.carp.maintenancemode }}" + - name: Resolve update-vs-upgrade specifics + ansible.builtin.set_fact: + needs_reboot: "{{ upgrade_needs_reboot if firmware_status == 'upgrade' else update_needs_reboot }}" + firmware_target_kind: "{{ 'series' if firmware_status == 'upgrade' else 'version' }}" + firmware_target_value: "{{ firmware_upgrade_version if firmware_status == 'upgrade' else firmware_update_version }}" + - name: Skip backup {{ firmware_action }} ansible.builtin.set_fact: skip_update: true @@ -230,7 +235,7 @@ force_basic_auth: true body: | Current version: {{ firmware_current_version }} - {{ firmware_action | capitalize }} to {{ firmware_new_version }} failed on {{ inventory_hostname }} ({{ main_role }}). + {{ firmware_action | capitalize }} to {{ firmware_update_version }} failed on {{ inventory_hostname }} ({{ main_role }}). No snapshot have been taken before the {{ firmware_action }}. headers: Title: "OPNsense {{ firmware_action }} failed on {{ inventory_hostname }}" @@ -258,7 +263,7 @@ vmid: "{{ proxmox_vmid }}" state: present snapname: "{{ proxmox_snap_name }}" - description: "Pre-firmware-{{ firmware_action }}: {{ firmware_current_version }} → {{ firmware_new_version }}" + description: "Pre-firmware-{{ firmware_action }}: {{ firmware_current_version }} → {{ firmware_update_version }}" - name: Enable CARP maintenance mode ansible.builtin.uri: @@ -365,7 +370,7 @@ force_basic_auth: true body: | Current version: {{ firmware_current_version }} - {{ firmware_action | capitalize }} to {{ firmware_new_version }} failed on {{ inventory_hostname }} ({{ main_role }}). + {{ firmware_action | capitalize }} to {{ firmware_update_version }} failed on {{ inventory_hostname }} ({{ main_role }}). # {% if } A snapshot have been taken before the {{ firmware_action }} and the VM has been rollback. headers: @@ -398,7 +403,7 @@ OPNsense HA cluster {{ firmware_action }} completed successfully. {% for host in [master, backup] %} {% set h = hostvars[host] %} - {{ h.main_role }} ({{ host }}): {% if h.skip_update %}already on {{ h.firmware_current_version }}, skipped{% else %}{{ h.firmware_current_version }} → {{ h.firmware_new_version }}{% endif %}{{ "\n" if not loop.last else "" }} + {{ h.main_role }} ({{ host }}): {% if h.skip_update %}already on {{ h.firmware_current_version }}, skipped{% else %}{{ h.firmware_current_version }} → {{ h.firmware_update_version }}{% endif %}{{ "\n" if not loop.last else "" }} {% endfor %} headers: Title: "OPNsense {{ firmware_action }} completed" -- 2.54.0 From 74fa6432dbe5e8a2c0e232927d3bb8b1e85e804d Mon Sep 17 00:00:00 2001 From: Vezpi Date: Sun, 28 Jun 2026 10:45:40 +0000 Subject: [PATCH 55/74] change: timeout after reboot from 300 to 900 --- ansible/opnsense/update_opnsense_ha_cluster.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 29b9d2e..fbc6438 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -313,7 +313,7 @@ host: "{{ ansible_host }}" port: "{{ opnsense_https_port }}" state: started - timeout: 300 + timeout: 900 delay: 30 when: needs_reboot @@ -371,7 +371,6 @@ body: | Current version: {{ firmware_current_version }} {{ firmware_action | capitalize }} to {{ firmware_update_version }} failed on {{ inventory_hostname }} ({{ main_role }}). - # {% if } A snapshot have been taken before the {{ firmware_action }} and the VM has been rollback. headers: Title: "OPNsense {{ firmware_action }} failed on {{ inventory_hostname }}" -- 2.54.0 From d08f207fc4e38430d18431abfd0ee64e9a60d38b Mon Sep 17 00:00:00 2001 From: Vezpi Date: Sun, 28 Jun 2026 16:49:25 +0200 Subject: [PATCH 56/74] change: timeout after reboot from 900 to 1800 --- ansible/opnsense/update_opnsense_ha_cluster.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index fbc6438..8cf39c9 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -186,7 +186,7 @@ host: "{{ ansible_host }}" port: "{{ opnsense_https_port }}" state: started - timeout: 300 + timeout: 1800 delay: 30 when: needs_reboot @@ -313,7 +313,7 @@ host: "{{ ansible_host }}" port: "{{ opnsense_https_port }}" state: started - timeout: 900 + timeout: 1800 delay: 30 when: needs_reboot -- 2.54.0 From 697ac311a77c2728b0469ee5bffdec4d4fdaaed2 Mon Sep 17 00:00:00 2001 From: Vezpi Date: Sun, 28 Jun 2026 17:28:17 +0200 Subject: [PATCH 57/74] fix: product dict --- ansible/opnsense/update_opnsense_ha_cluster.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 8cf39c9..59ae02e 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -199,7 +199,7 @@ force_basic_auth: true validate_certs: false register: _post_firmware_status - until: _post_firmware_status.json['product_' ~ firmware_target_kind] | default('unknown') == firmware_target_value + until: _post_firmware_status.json.product['product_' ~ firmware_target_kind] | default('unknown') == firmware_target_value retries: 240 delay: 15 @@ -326,8 +326,8 @@ force_basic_auth: true validate_certs: false register: _post_firmware_status - until: _post_firmware_status.json['product_' ~ firmware_target_kind] | default('unknown') == firmware_target_value - retries: 240 + until: _post_firmware_status.json.product['product_' ~ firmware_target_kind] | default('unknown') == firmware_target_value + retries: 2t40 delay: 15 - name: Check VIP status -- 2.54.0 From 6cf28240dfdd2fbf07b04f135a8a0a1ef6a36ee0 Mon Sep 17 00:00:00 2001 From: Vezpi Date: Sun, 28 Jun 2026 17:49:58 +0200 Subject: [PATCH 58/74] fix: typo --- ansible/opnsense/update_opnsense_ha_cluster.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 59ae02e..71c73ec 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -327,7 +327,7 @@ validate_certs: false register: _post_firmware_status until: _post_firmware_status.json.product['product_' ~ firmware_target_kind] | default('unknown') == firmware_target_value - retries: 2t40 + retries: 240 delay: 15 - name: Check VIP status -- 2.54.0 From cd0bfd956cdd8fab5d5a1f398c2a5f458f68aafa Mon Sep 17 00:00:00 2001 From: Vezpi Date: Sun, 28 Jun 2026 20:16:54 +0000 Subject: [PATCH 59/74] change: timeout from 1800 to 3600 --- ansible/opnsense/update_opnsense_ha_cluster.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 71c73ec..ea63d01 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -186,7 +186,7 @@ host: "{{ ansible_host }}" port: "{{ opnsense_https_port }}" state: started - timeout: 1800 + timeout: 3600 delay: 30 when: needs_reboot @@ -313,7 +313,7 @@ host: "{{ ansible_host }}" port: "{{ opnsense_https_port }}" state: started - timeout: 1800 + timeout: 3600 delay: 30 when: needs_reboot -- 2.54.0 From 1f5d99b5aea51f7a779a886bc541e106f952c300 Mon Sep 17 00:00:00 2001 From: Vezpi Date: Mon, 29 Jun 2026 20:47:37 +0000 Subject: [PATCH 60/74] change: timeout from 3600 to 5400 --- ansible/opnsense/update_opnsense_ha_cluster.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index ea63d01..569f4f1 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -186,7 +186,7 @@ host: "{{ ansible_host }}" port: "{{ opnsense_https_port }}" state: started - timeout: 3600 + timeout: 5400 delay: 30 when: needs_reboot @@ -313,7 +313,7 @@ host: "{{ ansible_host }}" port: "{{ opnsense_https_port }}" state: started - timeout: 3600 + timeout: 5400 delay: 30 when: needs_reboot -- 2.54.0 From 86e0b4a23fddde9877cea13d71073ed2e896825a Mon Sep 17 00:00:00 2001 From: Vezpi Date: Mon, 29 Jun 2026 20:52:00 +0000 Subject: [PATCH 61/74] fix: target version in notifications --- ansible/opnsense/update_opnsense_ha_cluster.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 569f4f1..c914ef4 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -235,7 +235,7 @@ force_basic_auth: true body: | Current version: {{ firmware_current_version }} - {{ firmware_action | capitalize }} to {{ firmware_update_version }} failed on {{ inventory_hostname }} ({{ main_role }}). + {{ firmware_action | capitalize }} to {{ firmware_target_value }} failed on {{ inventory_hostname }} ({{ main_role }}). No snapshot have been taken before the {{ firmware_action }}. headers: Title: "OPNsense {{ firmware_action }} failed on {{ inventory_hostname }}" @@ -263,7 +263,7 @@ vmid: "{{ proxmox_vmid }}" state: present snapname: "{{ proxmox_snap_name }}" - description: "Pre-firmware-{{ firmware_action }}: {{ firmware_current_version }} → {{ firmware_update_version }}" + description: "Pre-firmware-{{ firmware_action }}: {{ firmware_current_version }} → {{ firmware_target_value }}" - name: Enable CARP maintenance mode ansible.builtin.uri: @@ -370,7 +370,7 @@ force_basic_auth: true body: | Current version: {{ firmware_current_version }} - {{ firmware_action | capitalize }} to {{ firmware_update_version }} failed on {{ inventory_hostname }} ({{ main_role }}). + {{ firmware_action | capitalize }} to {{ firmware_target_value }} failed on {{ inventory_hostname }} ({{ main_role }}). A snapshot have been taken before the {{ firmware_action }} and the VM has been rollback. headers: Title: "OPNsense {{ firmware_action }} failed on {{ inventory_hostname }}" @@ -402,7 +402,7 @@ OPNsense HA cluster {{ firmware_action }} completed successfully. {% for host in [master, backup] %} {% set h = hostvars[host] %} - {{ h.main_role }} ({{ host }}): {% if h.skip_update %}already on {{ h.firmware_current_version }}, skipped{% else %}{{ h.firmware_current_version }} → {{ h.firmware_update_version }}{% endif %}{{ "\n" if not loop.last else "" }} + {{ h.main_role }} ({{ host }}): {% if h.skip_update %}already on {{ h.firmware_current_version }}, skipped{% else %}{{ h.firmware_current_version }} → {{ h.firmware_target_value }}{% endif %}{{ "\n" if not loop.last else "" }} {% endfor %} headers: Title: "OPNsense {{ firmware_action }} completed" -- 2.54.0 From 240791d22ef0d254b6733c16896f1522819ad81d Mon Sep 17 00:00:00 2001 From: Vezpi Date: Thu, 2 Jul 2026 20:19:20 +0000 Subject: [PATCH 62/74] fix: disable maintenance endpoint --- ansible/opnsense/update_opnsense_ha_cluster.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index c914ef4..a0cc85a 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -215,7 +215,7 @@ - name: Disable CARP maintenance mode ansible.builtin.uri: - url: "https://{{ opnsense_host }}/api/carp/status/maintenancemode" + url: "https://{{ opnsense_host }}/api/diagnostics/interface/carp_status/maintenance" method: POST user: "{{ opnsense_api_key }}" password: "{{ opnsense_api_secret }}" -- 2.54.0 From 2d9c6ffb1de671bf66d8192c89323059f5ba7843 Mon Sep 17 00:00:00 2001 From: Vezpi Date: Wed, 8 Jul 2026 19:43:06 +0000 Subject: [PATCH 63/74] change: use mwexecf instead of deprecated mwexec --- opnsense/10-wan | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/opnsense/10-wan b/opnsense/10-wan index 97dd098..055a120 100644 --- a/opnsense/10-wan +++ b/opnsense/10-wan @@ -84,7 +84,7 @@ if ($type === "MASTER") { write_config("disable interface '$ifkey' due CARP event '$type'", false); interface_configure(false, $ifkey, false, false); // Remove WAN default gateway - mwexec("/sbin/route delete default"); + mwexecf("/sbin/route delete default"); foreach ($config['OPNsense']['Gateways']['gateway_item'] as $gw) { if ($gw['name'] === $gw_name) { $gw_ip = $gw['gateway']; @@ -92,9 +92,9 @@ if ($type === "MASTER") { } } // Shutdown WAN interface - mwexec("/sbin/ifconfig {$real_if} down") + mwexecf("/sbin/ifconfig {$real_if} down") // Add fallback default gateway - mwexec("/sbin/route add default {$gw_ip}"); + mwexecf("/sbin/route add default {$gw_ip}"); // Create lock file file_put_contents($lock_file, time()); sleep($lock_default_age); -- 2.54.0 From c99279cb497e4f36a3e00aa55b057cbeffad9663 Mon Sep 17 00:00:00 2001 From: Vezpi Date: Sat, 11 Jul 2026 17:03:16 +0000 Subject: [PATCH 64/74] feat: rework notification --- .../opnsense/update_opnsense_ha_cluster.yml | 53 +++++++++---------- 1 file changed, 24 insertions(+), 29 deletions(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index a0cc85a..1e86d63 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -46,8 +46,8 @@ password: "{{ opnsense_api_secret }}" force_basic_auth: true validate_certs: false - register: _firmware_status - until: _firmware_status.json.status != 'none' + register: _firmware_status + until: _firmware_status.json.status_msg != "Firmware status requires to check for update first to provide more information." retries: 10 delay: 2 @@ -55,41 +55,42 @@ ansible.builtin.set_fact: firmware_action: "{{ opnsense_action | default('update')}}" firmware_status: "{{ _firmware_status.json.status }}" + firmware_status_msg: "{{ _firmware_status.json.status_msg }}" firmware_current_version: "{{ _firmware_status.json.product.product_version | default('unknown') }}" firmware_product_series: "{{ _firmware_status.json.product.product_series | default('unknown') }}" firmware_update_version: "{{ _firmware_status.json.upgrade_packages | selectattr('name', 'equalto', 'opnsense') | map(attribute='new_version') | first | default('') }}" firmware_upgrade_version: "{{ _firmware_status.json.upgrade_major_version | default('unknown') }}" firmware_upgrade_message: "{{ _firmware_status.json.upgrade_major_message | regex_replace('<[^>]+>', ' ') | regex_replace('\\s{2,}', ' ') | trim | default('') }}" - update_needs_reboot: "{{ _firmware_status.json.needs_reboot == '1' }}" - upgrade_needs_reboot: "{{ _firmware_status.json.upgrade_needs_reboot == '1' }}" + firmware_up_to_date: "{{ _firmware_status.json.status_msg == 'There are no updates available on the selected mirror.' }}" + needs_reboot: "{{ (_firmware_status.json.upgrade_needs_reboot == '1') if _firmware_status.json.status == 'upgrade' else (_firmware_status.json.needs_reboot == '1') }}" total_vips: "{{ _vip_status.json.rowCount }}" mismatched_vips: "{{ _vip_status.json.rows | rejectattr('status', 'equalto', main_role) | list | length }}" in_maintenance: "{{ _vip_status.json.carp.maintenancemode }}" - name: Resolve update-vs-upgrade specifics ansible.builtin.set_fact: - needs_reboot: "{{ upgrade_needs_reboot if firmware_status == 'upgrade' else update_needs_reboot }}" firmware_target_kind: "{{ 'series' if firmware_status == 'upgrade' else 'version' }}" firmware_target_value: "{{ firmware_upgrade_version if firmware_status == 'upgrade' else firmware_update_version }}" - - name: Skip backup {{ firmware_action }} + - name: Backup already updated ansible.builtin.set_fact: skip_update: true firmware_status: "skipped" delegate_to: "{{ groups['opnsense_backup'][0] }}" delegate_facts: true run_once: true - when: >- - (hostvars[groups['opnsense_backup'][0]] - ['firmware_current_version' - if hostvars[groups['opnsense_master'][0]].firmware_target_kind == 'version' - else 'firmware_product_series']) - == hostvars[groups['opnsense_master'][0]].firmware_target_value + when: (hostvars[groups['opnsense_backup'][0]].firmware_current_version if hostvars[groups['opnsense_master'][0]].firmware_target_kind == 'version' else 'firmware_product_series']) == hostvars[groups['opnsense_master'][0]].firmware_target_value or + + - name: Skip node {{ firmware_action }} + ansible.builtin.set_fact: + skip_update: true + firmware_status: "skipped" + when: firmware_up_to_date or + firmware_action != firmware_status - name: "Assert" ansible.builtin.assert: that: - - firmware_action == firmware_status or skip_update | default(false) - not in_maintenance - mismatched_vips == 0 - total_vips > 0 @@ -106,26 +107,19 @@ force_basic_auth: true body: | Current version: {{ firmware_current_version }} - {% if firmware_action != firmware_status %} - {% if firmware_status == "upgrade" %} - Upgrade to {{ firmware_upgrade_version }} available, but no upgrade requested. - {% if firmware_upgrade_message != "" %} - Upgrade message: - {{ firmware_upgrade_message }} - {% endif %} - {% else %} - No updates available on the cluster. - {% endif %} - {% elif in_maintenance %} + {% if in_maintenance %} The node {{ inventory_hostname }} is in CARP maintenance mode. {% elif mismatched_vips > 0 %} Some VIP are not {{ main_role }} on {{ ansible_host }}. - {% else %} + {% elif total_vips == 0 %} No VIPs are managed. + {% else %} + An error occured during the {{ firmware_action }}. {% endif %} headers: Title: "OPNsense {{ firmware_action }} report" Priority: "default" + Tags: "x" run_once: true when: ntfy_url is defined @@ -193,7 +187,7 @@ - name: Check firmware version ansible.builtin.uri: url: "https://{{ opnsense_host }}/api/core/firmware/status" - method: POST + method: GET user: "{{ opnsense_api_key }}" password: "{{ opnsense_api_secret }}" force_basic_auth: true @@ -256,7 +250,8 @@ tasks: - - block: + - when: not skip_update | default(false) + block: - name: Take Proxmox VM snapshot community.proxmox.proxmox_snap: @@ -320,7 +315,7 @@ - name: Check firmware version ansible.builtin.uri: url: "https://{{ opnsense_host }}/api/core/firmware/status" - method: POST + method: GET user: "{{ opnsense_api_key }}" password: "{{ opnsense_api_secret }}" force_basic_auth: true @@ -383,7 +378,7 @@ msg: "Update aborted" -- name: "Phase 4 | Notify on succes" +- name: "Phase 4 | Notification" hosts: localhost gather_facts: false tasks: -- 2.54.0 From 52eaeaae933bc43cfbfa86d9debbcf7b9611040a Mon Sep 17 00:00:00 2001 From: Vezpi Date: Sat, 11 Jul 2026 20:13:01 +0000 Subject: [PATCH 65/74] fix: syntax --- .../opnsense/update_opnsense_ha_cluster.yml | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 1e86d63..6548f64 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -79,14 +79,17 @@ delegate_to: "{{ groups['opnsense_backup'][0] }}" delegate_facts: true run_once: true - when: (hostvars[groups['opnsense_backup'][0]].firmware_current_version if hostvars[groups['opnsense_master'][0]].firmware_target_kind == 'version' else 'firmware_product_series']) == hostvars[groups['opnsense_master'][0]].firmware_target_value or + when: >- + (hostvars[groups['opnsense_backup'][0]].firmware_current_version + if hostvars[groups['opnsense_master'][0]].firmware_target_kind == 'version' + else hostvars[groups['opnsense_backup'][0]].firmware_product_series) + == hostvars[groups['opnsense_master'][0]].firmware_target_value - name: Skip node {{ firmware_action }} ansible.builtin.set_fact: skip_update: true firmware_status: "skipped" - when: firmware_up_to_date or - firmware_action != firmware_status + when: firmware_up_to_date or firmware_action != firmware_status - name: "Assert" ansible.builtin.assert: @@ -120,7 +123,6 @@ Title: "OPNsense {{ firmware_action }} report" Priority: "default" Tags: "x" - run_once: true when: ntfy_url is defined - ansible.builtin.fail: @@ -165,7 +167,9 @@ force_basic_auth: true validate_certs: false register: _update_running - failed_when: _update_running.json.status != 'busy' + until: _update_running.json.status == 'busy' + retries: 2 + delay: 2 - name: Wait for node to reboot after the {{ firmware_action }} ansible.builtin.wait_for: @@ -235,7 +239,6 @@ Title: "OPNsense {{ firmware_action }} failed on {{ inventory_hostname }}" Priority: "high" Tags: "x" - run_once: true when: ntfy_url is defined - ansible.builtin.fail: @@ -293,7 +296,9 @@ force_basic_auth: true validate_certs: false register: _update_running - failed_when: _update_running.json.status != 'busy' + until: _update_running.json.status == 'busy' + retries: 2 + delay: 2 - name: Wait for node to reboot after the {{ firmware_action }} ansible.builtin.wait_for: @@ -371,7 +376,6 @@ Title: "OPNsense {{ firmware_action }} failed on {{ inventory_hostname }}" Priority: "high" Tags: "x" - run_once: true when: ntfy_url is defined - ansible.builtin.fail: -- 2.54.0 From eccd9407270fd09fe888a156bc23f3be4137289d Mon Sep 17 00:00:00 2001 From: Vezpi Date: Tue, 14 Jul 2026 19:40:26 +0000 Subject: [PATCH 66/74] change: notification body --- .../opnsense/update_opnsense_ha_cluster.yml | 39 +++++++++++++++---- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 6548f64..47b0ea3 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -391,6 +391,10 @@ vars: master: "{{ groups['opnsense_master'][0] }}" backup: "{{ groups['opnsense_backup'][0] }}" + m: "{{ hostvars[master] }}" + b: "{{ hostvars[backup] }}" + same_operation: "{{ m.firmware_status == b.firmware_status }}" + ansible.builtin.uri: url: "{{ ntfy_url }}/{{ ntfy_topic }}" method: POST @@ -398,13 +402,32 @@ password: "{{ lookup('env', 'NTFY_PASSWORD') }}" force_basic_auth: true body: | - OPNsense HA cluster {{ firmware_action }} completed successfully. - {% for host in [master, backup] %} - {% set h = hostvars[host] %} - {{ h.main_role }} ({{ host }}): {% if h.skip_update %}already on {{ h.firmware_current_version }}, skipped{% else %}{{ h.firmware_current_version }} → {{ h.firmware_target_value }}{% endif %}{{ "\n" if not loop.last else "" }} - {% endfor %} + {%- if same_operation -%} + {%- if m.skip_update | default(false) -%} + Both nodes already on {{ m.firmware_current_version }}, no action taken. + {%- else -%} + OPNsense cluster: {{ m.firmware_current_version }} → {{ m.firmware_target_value }} ({{ m.firmware_status }}) + {%- endif -%} + {%- else -%} + {%- if m.skip_update | default(false) -%} + MASTER ({{ master }}): already on {{ m.firmware_current_version }}, no action taken. + {%- else -%} + MASTER ({{ master }}): {{ m.firmware_current_version }} → {{ m.firmware_target_value }} ({{ m.firmware_status }}) + {%- endif %} + {{ '\n' }} + {%- if b.skip_update | default(false) -%} + BACKUP ({{ backup }}): already on {{ b.firmware_current_version }}, no action taken. + {%- else -%} + BACKUP ({{ backup }}): {{ b.firmware_current_version }} → {{ b.firmware_target_value }} ({{ b.firmware_status }}) + {%- endif -%} + {%- endif -%} headers: - Title: "OPNsense {{ firmware_action }} completed" - Priority: "default" - Tags: "white_check_mark" + Title: | + {%- if m.skip_update | default(false) and b.skip_update | default(false) -%} + OPNsense cluster — no {{ _action }} available + {%- else -%} + OPNsense cluster {{ _action }} completed + {%- endif -%} + Priority: "{{ 'min' if (m.skip_update | default(false) and b.skip_update | default(false)) else 'default' }}" + Tags: "{{ 'information_source' if (m.skip_update | default(false) and b.skip_update | default(false)) else 'white_check_mark' }}" when: ntfy_url is defined \ No newline at end of file -- 2.54.0 From d7f3a7ef06a6210375e7fa600685858356ce0dad Mon Sep 17 00:00:00 2001 From: Vezpi Date: Tue, 14 Jul 2026 19:45:54 +0000 Subject: [PATCH 67/74] fix: typo --- ansible/opnsense/update_opnsense_ha_cluster.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 47b0ea3..629de20 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -424,9 +424,9 @@ headers: Title: | {%- if m.skip_update | default(false) and b.skip_update | default(false) -%} - OPNsense cluster — no {{ _action }} available + OPNsense cluster — no {{m.firmware_action }} available {%- else -%} - OPNsense cluster {{ _action }} completed + OPNsense cluster {{m.firmware_action }} completed {%- endif -%} Priority: "{{ 'min' if (m.skip_update | default(false) and b.skip_update | default(false)) else 'default' }}" Tags: "{{ 'information_source' if (m.skip_update | default(false) and b.skip_update | default(false)) else 'white_check_mark' }}" -- 2.54.0 From 6acf547df1736190ef46fa0bd4bb17ea991bebca Mon Sep 17 00:00:00 2001 From: Vezpi Date: Tue, 14 Jul 2026 19:49:56 +0000 Subject: [PATCH 68/74] fix: removed carriage return --- ansible/opnsense/update_opnsense_ha_cluster.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 629de20..4d34365 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -414,7 +414,6 @@ {%- else -%} MASTER ({{ master }}): {{ m.firmware_current_version }} → {{ m.firmware_target_value }} ({{ m.firmware_status }}) {%- endif %} - {{ '\n' }} {%- if b.skip_update | default(false) -%} BACKUP ({{ backup }}): already on {{ b.firmware_current_version }}, no action taken. {%- else -%} -- 2.54.0 From 52eed8aadef4c3fbc2e8d8fcdca3ed068b0ec901 Mon Sep 17 00:00:00 2001 From: Vezpi Date: Tue, 14 Jul 2026 19:58:45 +0000 Subject: [PATCH 69/74] fix: replace arrows --- ansible/opnsense/update_opnsense_ha_cluster.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 4d34365..43776cf 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -406,18 +406,18 @@ {%- if m.skip_update | default(false) -%} Both nodes already on {{ m.firmware_current_version }}, no action taken. {%- else -%} - OPNsense cluster: {{ m.firmware_current_version }} → {{ m.firmware_target_value }} ({{ m.firmware_status }}) + OPNsense cluster: {{ m.firmware_current_version }} -> {{ m.firmware_target_value }} ({{ m.firmware_status }}) {%- endif -%} {%- else -%} {%- if m.skip_update | default(false) -%} MASTER ({{ master }}): already on {{ m.firmware_current_version }}, no action taken. {%- else -%} - MASTER ({{ master }}): {{ m.firmware_current_version }} → {{ m.firmware_target_value }} ({{ m.firmware_status }}) + MASTER ({{ master }}): {{ m.firmware_current_version }} -> {{ m.firmware_target_value }} ({{ m.firmware_status }}) {%- endif %} {%- if b.skip_update | default(false) -%} BACKUP ({{ backup }}): already on {{ b.firmware_current_version }}, no action taken. {%- else -%} - BACKUP ({{ backup }}): {{ b.firmware_current_version }} → {{ b.firmware_target_value }} ({{ b.firmware_status }}) + BACKUP ({{ backup }}): {{ b.firmware_current_version }} -> {{ b.firmware_target_value }} ({{ b.firmware_status }}) {%- endif -%} {%- endif -%} headers: -- 2.54.0 From 2937ba4344d10ec9af3dd6202a567398152e7511 Mon Sep 17 00:00:00 2001 From: Vezpi Date: Tue, 14 Jul 2026 20:02:28 +0000 Subject: [PATCH 70/74] fix: replace dash --- ansible/opnsense/update_opnsense_ha_cluster.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 43776cf..27377b1 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -423,7 +423,7 @@ headers: Title: | {%- if m.skip_update | default(false) and b.skip_update | default(false) -%} - OPNsense cluster — no {{m.firmware_action }} available + OPNsense cluster - no {{m.firmware_action }} available {%- else -%} OPNsense cluster {{m.firmware_action }} completed {%- endif -%} -- 2.54.0 From 336f1fd4663b6a1e838f2d52501bc265f74e29b1 Mon Sep 17 00:00:00 2001 From: Vezpi Date: Tue, 14 Jul 2026 20:08:37 +0000 Subject: [PATCH 71/74] fix: replace block scalar --- ansible/opnsense/update_opnsense_ha_cluster.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 27377b1..34cf9a9 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -421,7 +421,7 @@ {%- endif -%} {%- endif -%} headers: - Title: | + Title: >- {%- if m.skip_update | default(false) and b.skip_update | default(false) -%} OPNsense cluster - no {{m.firmware_action }} available {%- else -%} -- 2.54.0 From 09862a53c1c7a073ab3e5a645205cc0ad88c08b4 Mon Sep 17 00:00:00 2001 From: Vezpi Date: Tue, 14 Jul 2026 20:24:40 +0000 Subject: [PATCH 72/74] fix: wording --- ansible/opnsense/update_opnsense_ha_cluster.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 34cf9a9..74a7fb4 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -406,18 +406,19 @@ {%- if m.skip_update | default(false) -%} Both nodes already on {{ m.firmware_current_version }}, no action taken. {%- else -%} - OPNsense cluster: {{ m.firmware_current_version }} -> {{ m.firmware_target_value }} ({{ m.firmware_status }}) + OPNsense cluster: {{ m.firmware_current_version }} → {{ m.firmware_target_value }} ({{ m.firmware_status }}) {%- endif -%} {%- else -%} {%- if m.skip_update | default(false) -%} MASTER ({{ master }}): already on {{ m.firmware_current_version }}, no action taken. {%- else -%} - MASTER ({{ master }}): {{ m.firmware_current_version }} -> {{ m.firmware_target_value }} ({{ m.firmware_status }}) + MASTER ({{ master }}): {{ m.firmware_current_version }} → {{ m.firmware_target_value }} ({{ m.firmware_status }}) {%- endif %} + \n {%- if b.skip_update | default(false) -%} BACKUP ({{ backup }}): already on {{ b.firmware_current_version }}, no action taken. {%- else -%} - BACKUP ({{ backup }}): {{ b.firmware_current_version }} -> {{ b.firmware_target_value }} ({{ b.firmware_status }}) + BACKUP ({{ backup }}): {{ b.firmware_current_version }} → {{ b.firmware_target_value }} ({{ b.firmware_status }}) {%- endif -%} {%- endif -%} headers: -- 2.54.0 From 29ba93e09624b3270067074891306ce04c462c93 Mon Sep 17 00:00:00 2001 From: Vezpi Date: Tue, 14 Jul 2026 20:30:57 +0000 Subject: [PATCH 73/74] fix: typo --- ansible/opnsense/update_opnsense_ha_cluster.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index 74a7fb4..f5a1386 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -414,7 +414,7 @@ {%- else -%} MASTER ({{ master }}): {{ m.firmware_current_version }} → {{ m.firmware_target_value }} ({{ m.firmware_status }}) {%- endif %} - \n + {{ '\n' }} {%- if b.skip_update | default(false) -%} BACKUP ({{ backup }}): already on {{ b.firmware_current_version }}, no action taken. {%- else -%} -- 2.54.0 From 2d1c4c9934057b71ac4280e4afd88f86f551666e Mon Sep 17 00:00:00 2001 From: Vezpi Date: Tue, 14 Jul 2026 20:36:24 +0000 Subject: [PATCH 74/74] fix: remove dashes in jinja conditions --- .../opnsense/update_opnsense_ha_cluster.yml | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/ansible/opnsense/update_opnsense_ha_cluster.yml b/ansible/opnsense/update_opnsense_ha_cluster.yml index f5a1386..233ae3f 100644 --- a/ansible/opnsense/update_opnsense_ha_cluster.yml +++ b/ansible/opnsense/update_opnsense_ha_cluster.yml @@ -402,25 +402,24 @@ password: "{{ lookup('env', 'NTFY_PASSWORD') }}" force_basic_auth: true body: | - {%- if same_operation -%} - {%- if m.skip_update | default(false) -%} + {% if same_operation %} + {% if m.skip_update | default(false) %} Both nodes already on {{ m.firmware_current_version }}, no action taken. - {%- else -%} + {% else %} OPNsense cluster: {{ m.firmware_current_version }} → {{ m.firmware_target_value }} ({{ m.firmware_status }}) - {%- endif -%} - {%- else -%} - {%- if m.skip_update | default(false) -%} + {% endif %} + {% else %} + {% if m.skip_update | default(false) %} MASTER ({{ master }}): already on {{ m.firmware_current_version }}, no action taken. - {%- else -%} + {% else %} MASTER ({{ master }}): {{ m.firmware_current_version }} → {{ m.firmware_target_value }} ({{ m.firmware_status }}) - {%- endif %} - {{ '\n' }} - {%- if b.skip_update | default(false) -%} + {% endif %} + {% if b.skip_update | default(false) %} BACKUP ({{ backup }}): already on {{ b.firmware_current_version }}, no action taken. - {%- else -%} + {% else %} BACKUP ({{ backup }}): {{ b.firmware_current_version }} → {{ b.firmware_target_value }} ({{ b.firmware_status }}) - {%- endif -%} - {%- endif -%} + {% endif %} + {% endif %} headers: Title: >- {%- if m.skip_update | default(false) and b.skip_update | default(false) -%} -- 2.54.0