--- # roles/pfsense_upgrade/tasks/carp.yml # Handles CARP/HA awareness during upgrades. # Only runs when ha_peer is defined. # # ha_role: backup → minimal pre-checks, upgrade proceeds normally # ha_role: primary → full CARP state verification, forced demotion, peer version check # --------------------------------------------------------------------------- # Backup node logic — runs on the backup before it upgrades # --------------------------------------------------------------------------- - name: "[CARP/backup] Verify primary peer is reachable before upgrading backup" ansible.builtin.raw: echo "ping" delegate_to: "{{ ha_peer }}" register: _peer_reachable failed_when: _peer_reachable.rc != 0 when: ha_role == 'backup' - name: "[CARP/backup] Check current CARP state on this node" ansible.builtin.raw: > php -r "require_once('/etc/inc/interfaces.inc'); \$carp = get_carp_status(); echo \$carp;" register: _backup_carp_state changed_when: false when: ha_role == 'backup' - name: "[CARP/backup] Display CARP state" ansible.builtin.debug: msg: "CARP state on {{ inventory_hostname }} (backup): {{ _backup_carp_state.stdout | trim }}" when: ha_role == 'backup' # --------------------------------------------------------------------------- # Primary node logic — runs on the primary before it upgrades # --------------------------------------------------------------------------- # --- Step 1: Verify peer is reachable --- - name: "[CARP/primary] Verify backup peer {{ ha_peer }} is reachable" ansible.builtin.raw: echo "ping" delegate_to: "{{ ha_peer }}" register: _peer_reachable failed_when: _peer_reachable.rc != 0 when: - ha_role == 'primary' - ha_peer is defined # --- Step 2: Verify peer is on the upgraded version --- - name: "[CARP/primary] Read version on backup peer {{ ha_peer }}" ansible.builtin.raw: cat {{ pfsense_version_file }} delegate_to: "{{ ha_peer }}" register: _peer_version_raw changed_when: false when: - ha_role == 'primary' - ha_peer is defined - name: "[CARP/primary] Set peer version fact" ansible.builtin.set_fact: ha_peer_version: "{{ _peer_version_raw.stdout | trim }}" when: - ha_role == 'primary' - ha_peer is defined - name: "[CARP/primary] Fail if backup peer is not on a newer version than primary" ansible.builtin.fail: msg: > Backup peer {{ ha_peer }} is running {{ ha_peer_version }}, which is the same as or older than this primary ({{ pfsense_current_version }}). Upgrade the backup node first before proceeding with the primary. when: - ha_role == 'primary' - ha_peer is defined - ha_peer_version == pfsense_current_version # --- Step 3: Verify backup peer is in MASTER CARP state --- - name: "[CARP/primary] Check CARP state on backup peer {{ ha_peer }}" ansible.builtin.raw: > ifconfig | grep -o 'carp: [A-Z]*' | awk '{print $2}' | sort -u delegate_to: "{{ ha_peer }}" register: _peer_carp_state_raw changed_when: false when: - ha_role == 'primary' - ha_peer is defined - name: "[CARP/primary] Fail if backup peer is not MASTER" ansible.builtin.fail: msg: > Backup peer {{ ha_peer }} CARP state is '{{ _peer_carp_state_raw.stdout | trim }}'. Expected MASTER. Resolve CARP state before upgrading the primary. when: - ha_role == 'primary' - ha_peer is defined - "'MASTER' not in _peer_carp_state_raw.stdout" # --- Step 4: Force demotion of primary --- - name: "[CARP/primary] Force CARP demotion on this node (set advskew to 254)" ansible.builtin.raw: > php -r " require_once('/etc/inc/interfaces.inc'); interfaces_carp_set_maintenancemode(true); " register: _carp_demotion changed_when: true when: - ha_role == 'primary' - ha_peer is defined - name: "[CARP/primary] Wait for CARP failover to settle" ansible.builtin.pause: seconds: 15 # --- Step 5: Re-verify backup peer has taken MASTER --- - name: "[CARP/primary] Re-check CARP state on backup peer after demotion" ansible.builtin.raw: > ifconfig | grep -o 'carp: [A-Z]*' | awk '{print $2}' | sort -u delegate_to: "{{ ha_peer }}" register: _peer_carp_recheck changed_when: false when: - ha_role == 'primary' - ha_peer is defined - name: "[CARP/primary] Fail if backup peer did not take MASTER after demotion" ansible.builtin.fail: msg: > Backup peer {{ ha_peer }} did not take MASTER after primary demotion. CARP state is '{{ _peer_carp_recheck.stdout | trim }}'. Investigate CARP before proceeding — primary has been demoted but backup is not MASTER. when: - ha_role == 'primary' - ha_peer is defined - "'MASTER' not in _peer_carp_recheck.stdout" - name: "[CARP/primary] CARP demotion confirmed — backup is MASTER, safe to upgrade primary" ansible.builtin.debug: msg: > {{ ha_peer }} is MASTER. {{ inventory_hostname }} is demoted. Proceeding with primary upgrade. when: - ha_role == 'primary' - ha_peer is defined # --------------------------------------------------------------------------- # Post-upgrade: restore CARP on primary and verify state # --------------------------------------------------------------------------- - name: "[CARP/primary] Re-enable CARP maintenance mode off (restore advskew)" ansible.builtin.raw: > php -r " require_once('/etc/inc/interfaces.inc'); interfaces_carp_set_maintenancemode(false); " register: _carp_restore changed_when: true when: - ha_role == 'primary' - ha_peer is defined - name: "[CARP/primary] Wait for CARP state to stabilize after restore" ansible.builtin.pause: seconds: 20 - name: "[CARP/primary] Verify primary has reclaimed MASTER" ansible.builtin.raw: > ifconfig | grep -o 'carp: [A-Z]*' | awk '{print $2}' | sort -u register: _primary_carp_final changed_when: false when: - ha_role == 'primary' - ha_peer is defined - name: "[CARP/primary] Warn if primary did not reclaim MASTER" ansible.builtin.debug: msg: > WARNING: Primary CARP state is '{{ _primary_carp_final.stdout | trim }}' — expected MASTER. This may resolve on its own. Check CARP status on both nodes manually. when: - ha_role == 'primary' - ha_peer is defined - "'MASTER' not in _primary_carp_final.stdout" - name: "[CARP/primary] CARP state confirmed restored" ansible.builtin.debug: msg: "{{ inventory_hostname }} has reclaimed MASTER. HA pair is fully operational." when: - ha_role == 'primary' - ha_peer is defined - "'MASTER' in _primary_carp_final.stdout"