188 lines
6.5 KiB
YAML
188 lines
6.5 KiB
YAML
---
|
|
# 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" |