Added pfsense upgrade roles
This commit is contained in:
139
roles/pfsense_upgrade/README.md
Normal file
139
roles/pfsense_upgrade/README.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# pfSense Ansible Upgrade Playbook
|
||||
|
||||
Upgrades pfSense CE (and pfSense Plus) systems safely via Ansible.
|
||||
|
||||
## Features
|
||||
|
||||
- **Version detection** — reads `/etc/version`, parses branch, patch level, and release type
|
||||
- **In-branch update check** — runs `pfSense-upgrade -c` to detect available patch releases within your running branch (e.g. 2.7.2 → 2.7.3)
|
||||
- **New stable branch detection** — queries upstream GitHub for the latest stable version and warns if a newer branch (e.g. 2.8.x) has been released
|
||||
- **Safety gates** — upgrade is a no-op unless `perform_upgrade=true` is explicitly passed
|
||||
- **Branch-crossing protection** — requires `allow_major_upgrade=true` to upgrade across major/minor branches
|
||||
- **Pre-upgrade config backup** — triggers pfSense's own PHP backup function before touching anything
|
||||
- **Serial execution** — `serial: 1` ensures HA pairs are never upgraded simultaneously
|
||||
- **Post-upgrade verification** — confirms version changed, services are listening, and the system is up-to-date
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
- Ansible ≥ 2.12
|
||||
- SSH enabled on pfSense (`System → Advanced → Admin Access → Enable Secure Shell`)
|
||||
- Admin user with shell access
|
||||
- The `raw` module is used throughout — **Python is not required on pfSense**
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Configure inventory
|
||||
|
||||
Edit `inventory/hosts.yml` with your pfSense host IPs and SSH user:
|
||||
|
||||
```yaml
|
||||
fw-site-a:
|
||||
ansible_host: 192.168.1.1
|
||||
ansible_user: admin
|
||||
```
|
||||
|
||||
### 2. Configure SSH auth
|
||||
|
||||
Either set a private key in `inventory/group_vars/pfsense.yml`:
|
||||
|
||||
```yaml
|
||||
ansible_ssh_private_key_file: ~/.ssh/pfsense_rsa
|
||||
```
|
||||
|
||||
Or use password auth (add `ansible_password: yourpassword` per host in `host_vars/`).
|
||||
|
||||
### 3. Check for available upgrades (safe, no changes made)
|
||||
|
||||
```bash
|
||||
ansible-playbook upgrade.yml
|
||||
```
|
||||
|
||||
Or with explicit tag:
|
||||
|
||||
```bash
|
||||
ansible-playbook upgrade.yml --tags check
|
||||
```
|
||||
|
||||
### 4. Apply in-branch upgrades
|
||||
|
||||
```bash
|
||||
ansible-playbook upgrade.yml -e "perform_upgrade=true"
|
||||
```
|
||||
|
||||
### 5. Apply upgrade and allow branch crossing (e.g. 2.7 → 2.8)
|
||||
|
||||
```bash
|
||||
ansible-playbook upgrade.yml -e "perform_upgrade=true allow_major_upgrade=true"
|
||||
```
|
||||
|
||||
### 6. Target a single host
|
||||
|
||||
```bash
|
||||
ansible-playbook upgrade.yml -l fw-site-a -e "perform_upgrade=true"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Variables Reference
|
||||
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `perform_upgrade` | `false` | Safety gate — must be `true` to apply upgrades |
|
||||
| `allow_major_upgrade` | `false` | Permit upgrades across branch boundaries |
|
||||
| `auto_reboot` | `true` | Reboot automatically after upgrade |
|
||||
| `reboot_timeout` | `300` | Seconds to wait for host after reboot |
|
||||
| `upgrade_check_timeout` | `120` | Timeout for pfSense-upgrade check |
|
||||
| `pkg_repo_update` | `true` | Run `pkg update -f` before checking |
|
||||
| `skip_backup_check` | `false` | Skip pre-upgrade config backup step |
|
||||
| `notify_webhook_url` | `""` | Optional Slack/Teams webhook for results |
|
||||
| `pfsense_release_url` | GitHub raw URL | Where to fetch upstream latest version |
|
||||
|
||||
---
|
||||
|
||||
## How Update Detection Works
|
||||
|
||||
### In-branch updates
|
||||
`pfSense-upgrade -d -c` is run on the host. It checks the configured pfSense pkg repository (same branch as running system) and returns non-zero if a newer package set is available. The playbook parses the exit code and output to determine availability.
|
||||
|
||||
### New stable branch detection
|
||||
The playbook fetches the `version` file from the pfSense CE GitHub repository (`master` branch) using FreeBSD `fetch`. The major.minor of the upstream version is compared to the running system's branch. If they differ and upstream is newer, a warning is displayed regardless of `perform_upgrade`.
|
||||
|
||||
> Note: The GitHub master branch reflects the latest stable CE release. Adjust `pfsense_release_url` if you track a specific branch or use an internal mirror.
|
||||
|
||||
---
|
||||
|
||||
## HA / CARP Considerations
|
||||
|
||||
- `serial: 1` in the playbook ensures only one host upgrades at a time
|
||||
- For CARP HA pairs, upgrade the **secondary first** so the primary continues to pass traffic
|
||||
- Group your HA pairs in the inventory and order hosts accordingly
|
||||
- Consider adding a task to demote the primary before upgrading if you need zero-downtime
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
pfsense-upgrade/
|
||||
├── ansible.cfg
|
||||
├── upgrade.yml # Main playbook entry point
|
||||
├── inventory/
|
||||
│ ├── hosts.yml # Your pfSense hosts
|
||||
│ └── group_vars/
|
||||
│ └── pfsense.yml # Shared SSH + default vars
|
||||
└── roles/
|
||||
└── pfsense_upgrade/
|
||||
├── defaults/
|
||||
│ └── main.yml # All default variable values
|
||||
└── tasks/
|
||||
├── main.yml # Task orchestration
|
||||
├── preflight.yml # SSH check, disk space, binary check
|
||||
├── version_detect.yml # Read and parse current version
|
||||
├── update_check.yml # pfSense-upgrade check + upstream compare
|
||||
├── upgrade.yml # Backup + execute upgrade
|
||||
└── verify.yml # Post-reboot version + service check
|
||||
```
|
||||
12
roles/pfsense_upgrade/ansible.cfg
Normal file
12
roles/pfsense_upgrade/ansible.cfg
Normal file
@@ -0,0 +1,12 @@
|
||||
[defaults]
|
||||
inventory = inventory/hosts.yml
|
||||
roles_path = roles
|
||||
host_key_checking = False
|
||||
timeout = 30
|
||||
forks = 1 # Never upgrade pfSense hosts in parallel
|
||||
stdout_callback = yaml
|
||||
callbacks_enabled = timer, profile_tasks
|
||||
|
||||
[ssh_connection]
|
||||
ssh_args = -o ControlMaster=no -o ControlPersist=no
|
||||
pipelining = False # Disabled — pfSense/FreeBSD SSH may not support it
|
||||
29
roles/pfsense_upgrade/defaults/main.yml
Normal file
29
roles/pfsense_upgrade/defaults/main.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
# roles/pfsense_upgrade/defaults/main.yml
|
||||
# Override any of these in group_vars, host_vars, or at the CLI with -e
|
||||
|
||||
# --- Safety gates ---
|
||||
perform_upgrade: false # Must explicitly set to true to apply upgrades
|
||||
allow_major_upgrade: false # Set true to permit branch-crossing upgrades (e.g. 2.7 → 2.8)
|
||||
skip_backup_check: false # Set true to skip the pre-upgrade config backup step
|
||||
|
||||
# --- Upgrade behavior ---
|
||||
auto_reboot: true # Reboot automatically after upgrade if required
|
||||
reboot_timeout: 300 # Seconds to wait for host to come back after reboot
|
||||
upgrade_check_timeout: 120 # Timeout for pfSense-upgrade version check
|
||||
pkg_repo_update: true # Run pkg update before checking for upgrades
|
||||
|
||||
# --- Notification ---
|
||||
# Optional: set to a Slack/Teams webhook URL to post upgrade results
|
||||
notify_webhook_url: ""
|
||||
|
||||
# --- pfSense paths ---
|
||||
pfsense_version_file: /etc/version
|
||||
pfsense_version_patch_file: /etc/version.patch
|
||||
pfsense_version_buildtime: /etc/version.buildtime
|
||||
pfsense_upgrade_bin: /usr/local/sbin/pfSense-upgrade
|
||||
pfsense_config_backup_path: /cf/conf/backup
|
||||
|
||||
# --- Release tracking ---
|
||||
# Netgate publishes release notes/versions at this URL (CE edition)
|
||||
pfsense_release_url: "https://raw.githubusercontent.com/pfsense/pfsense/master/src/etc/version"
|
||||
29
roles/pfsense_upgrade/hosts.yml
Normal file
29
roles/pfsense_upgrade/hosts.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
# inventory/hosts.yml
|
||||
# Define your pfSense hosts here.
|
||||
# Group them by site, role, or redundancy pair as needed.
|
||||
|
||||
all:
|
||||
children:
|
||||
pfsense:
|
||||
children:
|
||||
|
||||
# --- Primary/standalone firewalls ---
|
||||
pfsense_primary:
|
||||
hosts:
|
||||
fw-site-a:
|
||||
ansible_host: 192.168.1.1
|
||||
ansible_user: admin
|
||||
fw-site-b:
|
||||
ansible_host: 10.10.0.1
|
||||
ansible_user: admin
|
||||
|
||||
# --- HA pairs (upgrade sequentially — serial: 1 ensures this) ---
|
||||
pfsense_ha_pair_1:
|
||||
hosts:
|
||||
fw-ha-primary:
|
||||
ansible_host: 172.16.0.1
|
||||
ansible_user: admin
|
||||
fw-ha-secondary:
|
||||
ansible_host: 172.16.0.2
|
||||
ansible_user: admin
|
||||
188
roles/pfsense_upgrade/tasks/carp.yml
Normal file
188
roles/pfsense_upgrade/tasks/carp.yml
Normal file
@@ -0,0 +1,188 @@
|
||||
---
|
||||
# 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"
|
||||
36
roles/pfsense_upgrade/tasks/main.yml
Normal file
36
roles/pfsense_upgrade/tasks/main.yml
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
# roles/pfsense_upgrade/tasks/main.yml
|
||||
|
||||
- name: Include pre-flight checks
|
||||
ansible.builtin.import_tasks: preflight.yml
|
||||
tags: [always, preflight, check]
|
||||
|
||||
- name: Include version detection
|
||||
ansible.builtin.import_tasks: version_detect.yml
|
||||
tags: [always, check]
|
||||
|
||||
- name: Include update check
|
||||
ansible.builtin.import_tasks: update_check.yml
|
||||
tags: [always, check]
|
||||
|
||||
- name: Include CARP/HA pre-upgrade logic
|
||||
ansible.builtin.import_tasks: carp.yml
|
||||
tags: [always, check, carp]
|
||||
when: ha_peer is defined
|
||||
|
||||
- name: Include upgrade execution
|
||||
ansible.builtin.import_tasks: upgrade.yml
|
||||
tags: [upgrade]
|
||||
when: perform_upgrade | bool
|
||||
|
||||
- name: Include CARP/HA post-upgrade restore
|
||||
ansible.builtin.import_tasks: carp.yml
|
||||
tags: [upgrade, carp]
|
||||
when:
|
||||
- ha_peer is defined
|
||||
- perform_upgrade | bool
|
||||
|
||||
- name: Include post-upgrade verification
|
||||
ansible.builtin.import_tasks: verify.yml
|
||||
tags: [upgrade, verify]
|
||||
when: perform_upgrade | bool
|
||||
41
roles/pfsense_upgrade/tasks/preflight.yml
Normal file
41
roles/pfsense_upgrade/tasks/preflight.yml
Normal file
@@ -0,0 +1,41 @@
|
||||
---
|
||||
# roles/pfsense_upgrade/tasks/preflight.yml
|
||||
# Validate SSH connectivity, confirm host is pfSense, check disk space.
|
||||
|
||||
- name: Verify SSH connectivity to pfSense host
|
||||
ansible.builtin.raw: echo "ping"
|
||||
register: _ssh_test
|
||||
changed_when: false
|
||||
failed_when: _ssh_test.rc != 0
|
||||
|
||||
- name: Confirm host is running pfSense (check version file)
|
||||
ansible.builtin.raw: test -f {{ pfsense_version_file }} && echo "pfsense_ok"
|
||||
register: _pfsense_check
|
||||
changed_when: false
|
||||
failed_when: "'pfsense_ok' not in _pfsense_check.stdout"
|
||||
|
||||
- name: Check available disk space on root filesystem (must be ≥ 200 MB)
|
||||
ansible.builtin.raw: >
|
||||
df -m / | awk 'NR==2 {print $4}'
|
||||
register: _disk_avail
|
||||
changed_when: false
|
||||
|
||||
- name: Fail if disk space is insufficient
|
||||
ansible.builtin.fail:
|
||||
msg: >
|
||||
Host {{ inventory_hostname }} only has {{ _disk_avail.stdout | trim }} MB free on /.
|
||||
At least 200 MB is required to safely upgrade pfSense.
|
||||
when: (_disk_avail.stdout | trim | int) < 200
|
||||
|
||||
- name: Check that pfSense-upgrade binary exists
|
||||
ansible.builtin.raw: test -x {{ pfsense_upgrade_bin }} && echo "bin_ok"
|
||||
register: _bin_check
|
||||
changed_when: false
|
||||
failed_when: "'bin_ok' not in _bin_check.stdout"
|
||||
|
||||
- name: Pre-flight summary
|
||||
ansible.builtin.debug:
|
||||
msg: >
|
||||
Pre-flight OK — {{ inventory_hostname }}:
|
||||
disk free={{ _disk_avail.stdout | trim }}MB,
|
||||
pfSense-upgrade binary present.
|
||||
149
roles/pfsense_upgrade/tasks/update_check.yml
Normal file
149
roles/pfsense_upgrade/tasks/update_check.yml
Normal file
@@ -0,0 +1,149 @@
|
||||
---
|
||||
# roles/pfsense_upgrade/tasks/update_check.yml
|
||||
# Checks for available upgrades using pfSense-upgrade -c and pkg version.
|
||||
# Also queries upstream for the latest stable release on this branch.
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. Refresh the local pkg repository metadata
|
||||
# ---------------------------------------------------------------------------
|
||||
- name: Update pkg repository metadata
|
||||
ansible.builtin.raw: pkg update -f 2>&1
|
||||
register: _pkg_update
|
||||
changed_when: false
|
||||
when: pkg_repo_update | bool
|
||||
timeout: "{{ upgrade_check_timeout }}"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2. Run pfSense-upgrade in check-only mode
|
||||
# ---------------------------------------------------------------------------
|
||||
- name: Run pfSense-upgrade --check (dry run)
|
||||
ansible.builtin.raw: >
|
||||
{{ pfsense_upgrade_bin }} -d -c 2>&1
|
||||
register: _upgrade_check
|
||||
changed_when: false
|
||||
timeout: "{{ upgrade_check_timeout }}"
|
||||
# pfSense-upgrade exits 0 when up-to-date, non-zero when upgrade available.
|
||||
# We capture both cases.
|
||||
failed_when: false
|
||||
|
||||
- name: Parse upgrade check output
|
||||
ansible.builtin.set_fact:
|
||||
upgrade_check_stdout: "{{ _upgrade_check.stdout | trim }}"
|
||||
upgrade_check_rc: "{{ _upgrade_check.rc }}"
|
||||
# True if the tool reports an update is available
|
||||
upgrade_available: >-
|
||||
{{
|
||||
_upgrade_check.rc != 0 or
|
||||
'Upgraded' in _upgrade_check.stdout or
|
||||
'update' in _upgrade_check.stdout | lower and
|
||||
'up to date' not in _upgrade_check.stdout | lower
|
||||
}}
|
||||
# Attempt to extract the new version string from the upgrade check output
|
||||
# pfSense-upgrade typically prints: "pfSense-upgrade: New version available: 2.7.3-RELEASE"
|
||||
upgrade_available_version: >-
|
||||
{{
|
||||
_upgrade_check.stdout
|
||||
| regex_search('(\d+\.\d+\.\d+[-a-zA-Z0-9]*)', '\1')
|
||||
| first | default('unknown')
|
||||
}}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3. Check pkg for pending package updates (captures sub-component updates)
|
||||
# ---------------------------------------------------------------------------
|
||||
- name: Check for pending pkg upgrades (outdated packages)
|
||||
ansible.builtin.raw: pkg version -l '<' 2>&1 | head -40
|
||||
register: _pkg_outdated
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Count outdated packages
|
||||
ansible.builtin.set_fact:
|
||||
pkg_outdated_count: "{{ _pkg_outdated.stdout_lines | reject('match', '^\\s*$') | list | length }}"
|
||||
pkg_outdated_list: "{{ _pkg_outdated.stdout | trim }}"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 4. Detect the latest stable release for this branch via GitHub
|
||||
# ---------------------------------------------------------------------------
|
||||
- name: Fetch latest stable release version from Netgate/pfSense repo
|
||||
ansible.builtin.raw: >
|
||||
fetch -q -o - "{{ pfsense_release_url }}" 2>/dev/null || echo "fetch_failed"
|
||||
register: _upstream_version_raw
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Parse upstream latest stable version
|
||||
ansible.builtin.set_fact:
|
||||
upstream_version: "{{ _upstream_version_raw.stdout | trim }}"
|
||||
upstream_fetch_ok: "{{ 'fetch_failed' not in _upstream_version_raw.stdout }}"
|
||||
|
||||
- name: Derive upstream branch (major.minor)
|
||||
ansible.builtin.set_fact:
|
||||
upstream_major_minor: >-
|
||||
{{
|
||||
upstream_version
|
||||
| regex_replace('^(\d+\.\d+).*$', '\1')
|
||||
| default(pfsense_major_minor)
|
||||
}}
|
||||
when: upstream_fetch_ok | bool
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 5. Compare branches — detect if a newer stable branch exists upstream
|
||||
# ---------------------------------------------------------------------------
|
||||
- name: Determine if a newer major release branch is available
|
||||
ansible.builtin.set_fact:
|
||||
new_major_release_available: >-
|
||||
{{
|
||||
upstream_fetch_ok | bool and
|
||||
(upstream_major_minor | string) != (pfsense_major_minor | string) and
|
||||
(upstream_major_minor.split('.')[0] | int > pfsense_major_minor.split('.')[0] | int) or
|
||||
(upstream_major_minor.split('.')[0] | int == pfsense_major_minor.split('.')[0] | int and
|
||||
upstream_major_minor.split('.')[1] | int > pfsense_major_minor.split('.')[1] | int)
|
||||
}}
|
||||
when: upstream_fetch_ok | bool
|
||||
|
||||
- name: Default new_major_release_available when fetch failed
|
||||
ansible.builtin.set_fact:
|
||||
new_major_release_available: false
|
||||
when: not (upstream_fetch_ok | bool)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 6. Print the full update status report
|
||||
# ---------------------------------------------------------------------------
|
||||
- name: Display update status report
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "============================================================"
|
||||
- " Update Status: {{ inventory_hostname }}"
|
||||
- "============================================================"
|
||||
- " Current version : {{ pfsense_current_version }}"
|
||||
- " Current branch : {{ pfsense_major_minor }}"
|
||||
- "------------------------------------------------------------"
|
||||
- " In-branch update : {{ 'YES — ' ~ upgrade_available_version if upgrade_available | bool else 'No — already up to date' }}"
|
||||
- " Outdated pkgs : {{ pkg_outdated_count }} package(s) behind"
|
||||
- "------------------------------------------------------------"
|
||||
- " Upstream latest : {{ upstream_version if upstream_fetch_ok | bool else 'Could not reach upstream' }}"
|
||||
- " Upstream branch : {{ upstream_major_minor if upstream_fetch_ok | bool else 'N/A' }}"
|
||||
- " New branch avail : {{ 'YES — ' ~ upstream_version if new_major_release_available | bool else 'No' }}"
|
||||
- "------------------------------------------------------------"
|
||||
- " perform_upgrade : {{ perform_upgrade | bool }}"
|
||||
- " allow_major_upg : {{ allow_major_upgrade | bool }}"
|
||||
- "============================================================"
|
||||
|
||||
- name: Warn if a new major release branch is available but not allowed
|
||||
ansible.builtin.debug:
|
||||
msg: >
|
||||
WARNING: pfSense {{ upstream_version }} is available on branch {{ upstream_major_minor }},
|
||||
which is newer than your running branch {{ pfsense_major_minor }}.
|
||||
To upgrade across branches, re-run with: -e "perform_upgrade=true allow_major_upgrade=true"
|
||||
when:
|
||||
- new_major_release_available | bool
|
||||
- not (allow_major_upgrade | bool)
|
||||
|
||||
- name: Warn if perform_upgrade is false but upgrades are available
|
||||
ansible.builtin.debug:
|
||||
msg: >
|
||||
DRY RUN — upgrades are available but perform_upgrade=false.
|
||||
Re-run with -e "perform_upgrade=true" to apply.
|
||||
when:
|
||||
- (upgrade_available | bool) or (pkg_outdated_count | int > 0)
|
||||
- not (perform_upgrade | bool)
|
||||
100
roles/pfsense_upgrade/tasks/upgrade.yml
Normal file
100
roles/pfsense_upgrade/tasks/upgrade.yml
Normal file
@@ -0,0 +1,100 @@
|
||||
---
|
||||
# roles/pfsense_upgrade/tasks/upgrade.yml
|
||||
# Applies the upgrade after safety checks pass.
|
||||
# Only runs when perform_upgrade=true.
|
||||
|
||||
- name: Abort if no upgrade is available (nothing to do)
|
||||
ansible.builtin.debug:
|
||||
msg: >
|
||||
No in-branch upgrade is available for {{ inventory_hostname }}.
|
||||
Current version {{ pfsense_current_version }} is already the latest on branch {{ pfsense_major_minor }}.
|
||||
Skipping upgrade.
|
||||
when:
|
||||
- not (upgrade_available | bool)
|
||||
- not (new_major_release_available | bool and allow_major_upgrade | bool)
|
||||
|
||||
- name: End play for this host if nothing to upgrade
|
||||
ansible.builtin.meta: end_host
|
||||
when:
|
||||
- not (upgrade_available | bool)
|
||||
- not (new_major_release_available | bool and allow_major_upgrade | bool)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Branch-crossing guard
|
||||
# ---------------------------------------------------------------------------
|
||||
- name: Abort if major upgrade is available but not explicitly allowed
|
||||
ansible.builtin.fail:
|
||||
msg: >
|
||||
A new pfSense branch is available ({{ upstream_version }}) but allow_major_upgrade=false.
|
||||
Review the release notes for {{ upstream_version }} before upgrading across branches.
|
||||
Re-run with -e "allow_major_upgrade=true" when ready.
|
||||
when:
|
||||
- new_major_release_available | bool
|
||||
- not (allow_major_upgrade | bool)
|
||||
- not (upgrade_available | bool)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pre-upgrade config backup
|
||||
# ---------------------------------------------------------------------------
|
||||
- name: Trigger config backup via PHP (writes to /cf/conf/backup/)
|
||||
ansible.builtin.raw: >
|
||||
php -r "require_once('/etc/inc/config.inc');
|
||||
require_once('/etc/inc/util.inc');
|
||||
backup_config();"
|
||||
register: _backup_result
|
||||
changed_when: false
|
||||
when: not (skip_backup_check | bool)
|
||||
|
||||
- name: Confirm backup file was created
|
||||
ansible.builtin.raw: >
|
||||
ls -t {{ pfsense_config_backup_path }}/config-*.xml 2>/dev/null | head -1
|
||||
register: _backup_file
|
||||
changed_when: false
|
||||
when: not (skip_backup_check | bool)
|
||||
|
||||
- name: Display backup file path
|
||||
ansible.builtin.debug:
|
||||
msg: "Config backup written to: {{ _backup_file.stdout | trim }}"
|
||||
when:
|
||||
- not (skip_backup_check | bool)
|
||||
- _backup_file.stdout | trim | length > 0
|
||||
|
||||
- name: Warn if no backup file found
|
||||
ansible.builtin.debug:
|
||||
msg: >
|
||||
WARNING: Could not confirm config backup was written.
|
||||
Check {{ pfsense_config_backup_path }} manually before proceeding.
|
||||
when:
|
||||
- not (skip_backup_check | bool)
|
||||
- _backup_file.stdout | trim | length == 0
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Execute the upgrade
|
||||
# ---------------------------------------------------------------------------
|
||||
- name: "UPGRADE — Running pfSense-upgrade on {{ inventory_hostname }}"
|
||||
ansible.builtin.raw: >
|
||||
{{ pfsense_upgrade_bin }} -d -y 2>&1
|
||||
register: _upgrade_result
|
||||
async: 600 # pfSense upgrades can take several minutes
|
||||
poll: 10
|
||||
timeout: 620
|
||||
# The upgrade reboots the host — the connection will drop. That is expected.
|
||||
failed_when: >
|
||||
_upgrade_result.rc is defined and
|
||||
_upgrade_result.rc != 0 and
|
||||
'reboot' not in _upgrade_result.stdout | lower and
|
||||
'Restarting' not in _upgrade_result.stdout
|
||||
|
||||
- name: Display upgrade output
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ _upgrade_result.stdout_lines | default(['(no output captured — likely rebooted mid-stream)']) }}"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Wait for host to come back after reboot
|
||||
# ---------------------------------------------------------------------------
|
||||
- name: Wait for pfSense to reboot and become reachable
|
||||
ansible.builtin.wait_for_connection:
|
||||
delay: 30
|
||||
timeout: "{{ reboot_timeout }}"
|
||||
sleep: 10
|
||||
when: auto_reboot | bool
|
||||
62
roles/pfsense_upgrade/tasks/verify.yml
Normal file
62
roles/pfsense_upgrade/tasks/verify.yml
Normal file
@@ -0,0 +1,62 @@
|
||||
---
|
||||
# roles/pfsense_upgrade/tasks/verify.yml
|
||||
# Verifies the system is healthy after upgrade and reports the new version.
|
||||
|
||||
- name: Wait an additional grace period before verifying
|
||||
ansible.builtin.pause:
|
||||
seconds: 15
|
||||
|
||||
- name: Read post-upgrade version
|
||||
ansible.builtin.raw: cat {{ pfsense_version_file }}
|
||||
register: _new_version_raw
|
||||
changed_when: false
|
||||
retries: 3
|
||||
delay: 10
|
||||
|
||||
- name: Set post-upgrade version fact
|
||||
ansible.builtin.set_fact:
|
||||
pfsense_new_version: "{{ _new_version_raw.stdout | trim }}"
|
||||
|
||||
- name: Verify pfSense web GUI is responding (port 443)
|
||||
ansible.builtin.raw: >
|
||||
fetch -q -o /dev/null --no-verify-peer https://127.0.0.1/ 2>&1 || true
|
||||
register: _webgui_check
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Check that key pfSense services are running
|
||||
ansible.builtin.raw: >
|
||||
sockstat -l | grep -E ':(53|443|80)\b' | wc -l | tr -d ' '
|
||||
register: _services_check
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Run pfSense-upgrade --check post-upgrade (confirm up-to-date)
|
||||
ansible.builtin.raw: >
|
||||
{{ pfsense_upgrade_bin }} -d -c 2>&1
|
||||
register: _post_upgrade_check
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Upgrade result summary
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "============================================================"
|
||||
- " Upgrade Result: {{ inventory_hostname }}"
|
||||
- "============================================================"
|
||||
- " Previous version : {{ pfsense_current_version }}"
|
||||
- " New version : {{ pfsense_new_version }}"
|
||||
- " Version changed : {{ pfsense_current_version != pfsense_new_version }}"
|
||||
- " Listening ports : {{ _services_check.stdout | trim }} found (DNS/HTTP/HTTPS)"
|
||||
- " Post-upg check : {{ 'Up to date' if _post_upgrade_check.rc == 0 else 'May still have pending updates' }}"
|
||||
- "============================================================"
|
||||
|
||||
- name: Fail if version did not change after upgrade attempt
|
||||
ansible.builtin.fail:
|
||||
msg: >
|
||||
pfSense version on {{ inventory_hostname }} is still {{ pfsense_new_version }}
|
||||
after upgrade attempt (was {{ pfsense_current_version }}).
|
||||
The upgrade may not have applied correctly — check the host manually.
|
||||
when:
|
||||
- pfsense_current_version == pfsense_new_version
|
||||
- upgrade_available | bool
|
||||
70
roles/pfsense_upgrade/tasks/version_detect.yml
Normal file
70
roles/pfsense_upgrade/tasks/version_detect.yml
Normal file
@@ -0,0 +1,70 @@
|
||||
---
|
||||
# roles/pfsense_upgrade/tasks/version_detect.yml
|
||||
# Reads version info from the running pfSense host and sets facts.
|
||||
|
||||
- name: Read current pfSense version string
|
||||
ansible.builtin.raw: cat {{ pfsense_version_file }}
|
||||
register: _raw_version
|
||||
changed_when: false
|
||||
|
||||
- name: Read patch level (if present)
|
||||
ansible.builtin.raw: >
|
||||
test -f {{ pfsense_version_patch_file }} && cat {{ pfsense_version_patch_file }} || echo "0"
|
||||
register: _raw_patch
|
||||
changed_when: false
|
||||
|
||||
- name: Read build timestamp (if present)
|
||||
ansible.builtin.raw: >
|
||||
test -f {{ pfsense_version_buildtime }} && cat {{ pfsense_version_buildtime }} || echo "unknown"
|
||||
register: _raw_buildtime
|
||||
changed_when: false
|
||||
|
||||
- name: Read pfSense edition (CE vs Plus)
|
||||
ansible.builtin.raw: >
|
||||
pkg info pfSense 2>/dev/null | grep -i '^Name' | awk '{print $3}' || echo "pfSense"
|
||||
register: _raw_edition
|
||||
changed_when: false
|
||||
|
||||
- name: Detect CPU architecture
|
||||
ansible.builtin.raw: uname -m
|
||||
register: _raw_arch
|
||||
changed_when: false
|
||||
|
||||
- name: Set version facts
|
||||
ansible.builtin.set_fact:
|
||||
pfsense_current_version: "{{ _raw_version.stdout | trim }}"
|
||||
pfsense_current_patch: "{{ _raw_patch.stdout | trim }}"
|
||||
pfsense_build_time: "{{ _raw_buildtime.stdout | trim }}"
|
||||
pfsense_edition: "{{ _raw_edition.stdout | trim }}"
|
||||
pfsense_arch: "{{ _raw_arch.stdout | trim }}"
|
||||
# Parse major.minor from version string (e.g. "2.7.2-RELEASE" → "2.7")
|
||||
pfsense_major_minor: >-
|
||||
{{
|
||||
(_raw_version.stdout | trim)
|
||||
| regex_replace('^(\d+\.\d+).*$', '\1')
|
||||
}}
|
||||
# Parse patch version integer (e.g. "2.7.2-RELEASE" → 2)
|
||||
pfsense_patch_int: >-
|
||||
{{
|
||||
(_raw_version.stdout | trim)
|
||||
| regex_replace('^(\d+)\.(\d+)\.(\d+).*$', '\3')
|
||||
| default('0')
|
||||
}}
|
||||
# Determine if this is a RELEASE, RC, BETA, or ALPHA
|
||||
pfsense_release_type: >-
|
||||
{{
|
||||
(_raw_version.stdout | trim)
|
||||
| regex_replace('^.*-(RELEASE|RC\d*|BETA\d*|ALPHA\d*|DEVELOPMENT).*$', '\1')
|
||||
| default('UNKNOWN')
|
||||
}}
|
||||
|
||||
- name: Display detected version info
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "Host : {{ inventory_hostname }}"
|
||||
- "Edition : {{ pfsense_edition }}"
|
||||
- "Version : {{ pfsense_current_version }}"
|
||||
- "Branch : {{ pfsense_major_minor }}"
|
||||
- "Release type : {{ pfsense_release_type }}"
|
||||
- "Build time : {{ pfsense_build_time }}"
|
||||
- "Architecture : {{ pfsense_arch }}"
|
||||
Reference in New Issue
Block a user