166 lines
6.8 KiB
YAML
166 lines
6.8 KiB
YAML
# roles/pfsense_upgrade/tasks/carp_pre.yml
|
|
# Handles CARP/HA pre-upgrade logic: verification and demotion.
|
|
# Only runs when ha_peer is defined and perform_upgrade is true.
|
|
#
|
|
# ha_role: backup → minimal pre-checks, upgrade proceeds normally
|
|
# ha_role: primary → full CARP state verification, forced demotion
|
|
|
|
|
|
# Exit early if HA not needed
|
|
- name: "[CARP] Exit - No HA configured"
|
|
ansible.builtin.meta: end_play
|
|
when: ha_peer is not defined or ha_peer | length == 0
|
|
|
|
# Exit early if no upgrade available
|
|
- name: "[CARP] Exit - No upgrade available for this host"
|
|
ansible.builtin.meta: end_play
|
|
when: not (upgrade_available | default(false) | bool)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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] Verify backup is in BACKUP state (all VIPs)"
|
|
ansible.builtin.raw: |
|
|
php -r 'require_once("/etc/inc/config.inc"); require_once("/etc/inc/interfaces.inc"); $ready = true; foreach(config_get_path("virtualip/vip", []) as $vip) { if ($vip["mode"] != "carp") continue; if (get_carp_interface_status("_vip" . $vip["uniqid"]) != "BACKUP") { $ready = false; break; } } echo $ready ? "BACKUP_READY" : "NOT_READY";'
|
|
register: _backup_carp_state
|
|
changed_when: false
|
|
when: ha_role == 'backup'
|
|
|
|
- name: "[CARP/backup] Fail if backup is not fully in BACKUP state"
|
|
ansible.builtin.fail:
|
|
msg: "Backup node {{ inventory_hostname }} is not in BACKUP state for all CARP VIPs. State: {{ _backup_carp_state.stdout }}"
|
|
when:
|
|
- ha_role == 'backup'
|
|
- _backup_carp_state.stdout != "BACKUP_READY"
|
|
|
|
- name: "[CARP/backup] Display CARP state"
|
|
ansible.builtin.debug:
|
|
msg: "CARP state on {{ inventory_hostname }} (backup): All VIPs are BACKUP"
|
|
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] Warn if backup peer is not on newer version"
|
|
ansible.builtin.debug:
|
|
msg: |
|
|
⚠ WARNING: Backup peer {{ ha_peer }} is running {{ ha_peer_version }},
|
|
which is the SAME as primary ({{ pfsense_current_version }}).
|
|
|
|
Primary upgrade requires backup to be on a newer version first.
|
|
Upgrade the backup node before upgrading primary.
|
|
when:
|
|
- ha_role == 'primary'
|
|
- ha_peer is defined
|
|
- ha_peer_version == pfsense_current_version
|
|
|
|
- name: "[CARP/primary] Skip primary upgrade when backup not newer"
|
|
ansible.builtin.meta: end_play
|
|
when:
|
|
- ha_role == 'primary'
|
|
- ha_peer is defined
|
|
- ha_peer_version == pfsense_current_version
|
|
- perform_upgrade | bool
|
|
|
|
# --- Step 3: Verify backup peer is in MASTER CARP state ---
|
|
- name: "[CARP/primary] Verify backup peer is MASTER for all CARP VIPs"
|
|
ansible.builtin.raw: |
|
|
ssh {{ ha_peer }} 'php -r "require_once(\"/etc/inc/config.inc\"); require_once(\"/etc/inc/interfaces.inc\"); $all_master = true; foreach(config_get_path(\"virtualip/vip\", []) as $vip) { if ($vip[\"mode\"] != \"carp\") continue; if (get_carp_interface_status(\"_vip\" . $vip[\"uniqid\"]) != \"MASTER\") { $all_master = false; break; } } echo $all_master ? \"ALL_MASTER\" : \"NOT_ALL_MASTER\";"'
|
|
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 for all VIPs"
|
|
ansible.builtin.fail:
|
|
msg: >
|
|
Backup peer {{ ha_peer }} is not MASTER for all CARP VIPs.
|
|
State: {{ _peer_carp_state_raw.stdout }}.
|
|
Resolve CARP state before upgrading the primary.
|
|
when:
|
|
- ha_role == 'primary'
|
|
- ha_peer is defined
|
|
- _peer_carp_state_raw.stdout != "ALL_MASTER"
|
|
|
|
# --- Step 4: Force demotion of primary ---
|
|
- name: "[CARP/primary] Force CARP demotion on this node (enter maintenance mode)"
|
|
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: 30
|
|
when:
|
|
- ha_role == 'primary'
|
|
- ha_peer is defined
|
|
|
|
# --- Step 5: Re-verify backup peer has taken MASTER ---
|
|
- name: "[CARP/primary] Re-check backup peer MASTER state after demotion"
|
|
ansible.builtin.raw: |
|
|
ssh {{ ha_peer }} 'php -r "require_once(\"/etc/inc/config.inc\"); require_once(\"/etc/inc/interfaces.inc\"); $all_master = true; foreach(config_get_path(\"virtualip/vip\", []) as $vip) { if ($vip[\"mode\"] != \"carp\") continue; if (get_carp_interface_status(\"_vip\" . $vip[\"uniqid\"]) != \"MASTER\") { $all_master = false; break; } } echo $all_master ? \"ALL_MASTER\" : \"NOT_ALL_MASTER\";"'
|
|
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 become MASTER for all VIPs after primary demotion.
|
|
State: {{ _peer_carp_recheck.stdout }}.
|
|
Investigate CARP before proceeding — primary has been demoted but backup is not MASTER.
|
|
when:
|
|
- ha_role == 'primary'
|
|
- ha_peer is defined
|
|
- _peer_carp_recheck.stdout != "ALL_MASTER"
|
|
|
|
- name: "[CARP/primary] CARP demotion confirmed — backup is MASTER, safe to upgrade primary"
|
|
ansible.builtin.debug:
|
|
msg: >
|
|
{{ ha_peer }} is MASTER for all VIPs. {{ inventory_hostname }} is demoted.
|
|
Proceeding with primary upgrade.
|
|
when:
|
|
- ha_role == 'primary'
|
|
- ha_peer is defined |