From 738c9dd4a02c2c87f716f8e5a9089dd1e56268ac Mon Sep 17 00:00:00 2001 From: "Ben D." Date: Wed, 29 Apr 2026 12:50:19 -0700 Subject: [PATCH] Split out carp roles --- roles/pfsense_upgrade/tasks/carp.yml | 205 --------------------------- 1 file changed, 205 deletions(-) delete mode 100644 roles/pfsense_upgrade/tasks/carp.yml diff --git a/roles/pfsense_upgrade/tasks/carp.yml b/roles/pfsense_upgrade/tasks/carp.yml deleted file mode 100644 index 857036d..0000000 --- a/roles/pfsense_upgrade/tasks/carp.yml +++ /dev/null @@ -1,205 +0,0 @@ -# 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' - ### Not altered ### - -# --- REWRITTEN: Check CARP state on backup using native pfSense function --- -- 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 - ### Not altered ### - -# --- 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 - # No perform_upgrade condition - always end play when versions equal - -# --- Step 3: Verify backup peer is in MASTER CARP state (rewritten) --- -- 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 (rewritten for tcsh safety) --- -- 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 (rewritten) --- -- 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 - ### Not altered ### - -# --------------------------------------------------------------------------- -# Post-upgrade: restore CARP on primary and verify state -# --------------------------------------------------------------------------- -- name: "[CARP/primary] Re-enable CARP (exit maintenance mode)" - 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 - when: - - ha_role == 'primary' - - ha_peer is defined - -- name: "[CARP/primary] Verify primary has reclaimed MASTER for all VIPs" - ansible.builtin.raw: | - 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: _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 }}' — expected ALL_MASTER. - This may resolve on its own. Check CARP status on both nodes manually. - when: - - ha_role == 'primary' - - ha_peer is defined - - _primary_carp_final.stdout != "ALL_MASTER" - -- name: "[CARP/primary] CARP state confirmed restored" - ansible.builtin.debug: - msg: "{{ inventory_hostname }} has reclaimed MASTER for all VIPs. HA pair is fully operational." - when: - - ha_role == 'primary' - - ha_peer is defined - - _primary_carp_final.stdout == "ALL_MASTER" \ No newline at end of file