Added pfsense upgrade roles

This commit is contained in:
Ben D.
2026-04-27 13:15:56 -07:00
parent 1e26dd304b
commit 03e889051e
35 changed files with 956 additions and 8 deletions

5
.gitignore vendored
View File

@@ -6,3 +6,8 @@ __pycache__/
.ansible/
fact_cache/
*.swp
.DS_Store
.AppleDouble
.LSOverride
.Spotlight-V100
.fseventsd

BIN
inventories/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,30 @@
---
# inventory/group_vars/pfsense.yml
# Applied to all hosts in the [pfsense] group.
# pfSense runs FreeBSD — Python may not be installed.
# Using 'raw' module throughout the role avoids this entirely,
# but set the interpreter discovery to auto for safety.
ansible_python_interpreter: auto_silent
# SSH connection settings tuned for pfSense/FreeBSD
ansible_connection: ssh
ansible_ssh_common_args: >-
-o StrictHostKeyChecking=no
-o UserKnownHostsFile=/dev/null
-o ConnectTimeout=15
-o ServerAliveInterval=10
-o ServerAliveCountMax=3
# pfSense's shell is tcsh by default; force sh for compatibility
ansible_shell_type: sh
ansible_shell_executable: /bin/sh
# Set to your SSH key or use ansible_password
# ansible_ssh_private_key_file: ~/.ssh/pfsense_rsa
# Default upgrade settings (can be overridden per host in host_vars/)
perform_upgrade: false
allow_major_upgrade: false
auto_reboot: true
pkg_repo_update: true

View File

@@ -11,7 +11,7 @@ all:
human_estimate_seconds: 2700
change_freeze: false
ansible_ssh_extra_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
children:
# --- NETWORK LAYER ---
firewalls:
@@ -20,11 +20,14 @@ all:
hosts:
client-fw-01:
ansible_host: "{{ FW_HOST }}"
pfsense:
hosts:
client-fw-01:
ansible_host: "{{ FW_HOST }}"
ansible_port: 22222
ha_role: "primary"
#ha_peer: "client-fw-02" # Uncomment if this node is part an HA pair
# --- INFRASTRUCTURE ---
hypervisors:
@@ -37,7 +40,7 @@ all:
hosts:
client-xcp-01:
ansible_host: "{{ XCP_HOST }}"
# --- WORKSTATIONS/SERVERS ---
linux_hosts:
hosts: {}
@@ -54,4 +57,3 @@ all:
ansible_winrm_transport: ntlm
ansible_winrm_server_cert_validation: validate
ansible_port: 5986

BIN
inventories/clients/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,30 @@
---
# inventory/group_vars/pfsense.yml
# Applied to all hosts in the [pfsense] group.
# pfSense runs FreeBSD — Python may not be installed.
# Using 'raw' module throughout the role avoids this entirely,
# but set the interpreter discovery to auto for safety.
ansible_python_interpreter: auto_silent
# SSH connection settings tuned for pfSense/FreeBSD
ansible_connection: ssh
ansible_ssh_common_args: >-
-o StrictHostKeyChecking=no
-o UserKnownHostsFile=/dev/null
-o ConnectTimeout=15
-o ServerAliveInterval=10
-o ServerAliveCountMax=3
# pfSense's shell is tcsh by default; force sh for compatibility
ansible_shell_type: sh
ansible_shell_executable: /bin/sh
# Set to your SSH key or use ansible_password
# ansible_ssh_private_key_file: ~/.ssh/pfsense_rsa
# Default upgrade settings (can be overridden per host in host_vars/)
perform_upgrade: false
allow_major_upgrade: false
auto_reboot: true
pkg_repo_update: true

View File

@@ -22,7 +22,9 @@ all:
vendor: "pfsense"
ansible_host: "fw.brenex.com"
ansible_port: 22222
ha_role: "primary"
#ha_peer: "fw-ha-secondary" # Uncomment if this node is part of an HA pair
xcpng_pools:
vars:
ansible_become: false
@@ -32,7 +34,7 @@ all:
shared_storage: false
upgrade_order:
- brenex-pool-01
hosts:
brenex-pool-01:
ansible_host: 192.168.123.11
@@ -41,7 +43,7 @@ all:
vars:
ansible_user: root
os_family: "debian"
hosts:
caddy-server:
ansible_host: 192.168.123.16
@@ -52,7 +54,7 @@ all:
ansible_host: 192.168.123.146
graylog-server:
ansible_host: 192.168.123.16
windows_hosts:

View File

@@ -0,0 +1,24 @@
---
# pfSense Upgrade Playbook
# Upgrades pfSense systems within their current version branch.
# Detects available stable releases and reports or applies upgrades.
#
# Usage:
# ansible-playbook upgrade.yml -i inventory/hosts.yml
# ansible-playbook upgrade.yml -i inventory/hosts.yml --tags check # dry-run only
# ansible-playbook upgrade.yml -i inventory/hosts.yml -e "perform_upgrade=true"
# ansible-playbook upgrade.yml -i inventory/hosts.yml -e "perform_upgrade=true allow_major_upgrade=true"
- name: pfSense Upgrade
hosts: pfsense
gather_facts: false
serial: 1 # Upgrade one host at a time to preserve redundancy
vars:
perform_upgrade: false # Safety gate — must be explicitly set to true
allow_major_upgrade: false # Set true to allow crossing major version branches
reboot_timeout: 300 # Seconds to wait for host after reboot
upgrade_check_timeout: 120 # Seconds before pfSense-upgrade check times out
roles:
- pfsense_upgrade

BIN
roles/.DS_Store vendored Normal file

Binary file not shown.

BIN
roles/hypervisor_backup_config/.DS_Store vendored Normal file

Binary file not shown.

BIN
roles/linux_patch/.DS_Store vendored Normal file

Binary file not shown.

View 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
```

View 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

View 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"

View 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

View 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"

View 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

View 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.

View 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)

View 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

View 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

View 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 }}"

BIN
roles/preflight/.DS_Store vendored Normal file

Binary file not shown.

BIN
roles/proxmox_ceph/.DS_Store vendored Normal file

Binary file not shown.

BIN
roles/proxmox_config_backup/.DS_Store vendored Normal file

Binary file not shown.

BIN
roles/proxmox_drain/.DS_Store vendored Normal file

Binary file not shown.

BIN
roles/proxmox_ha/.DS_Store vendored Normal file

Binary file not shown.

BIN
roles/proxmox_preflight/.DS_Store vendored Normal file

Binary file not shown.

BIN
roles/proxmox_restore/.DS_Store vendored Normal file

Binary file not shown.

BIN
roles/proxmox_status/.DS_Store vendored Normal file

Binary file not shown.

BIN
roles/proxmox_upgrade/.DS_Store vendored Normal file

Binary file not shown.

BIN
roles/proxmox_upgrade_node/.DS_Store vendored Normal file

Binary file not shown.

BIN
roles/report/.DS_Store vendored Normal file

Binary file not shown.

BIN
roles/snapshot/.DS_Store vendored Normal file

Binary file not shown.

BIN
roles/windows_patch/.DS_Store vendored Normal file

Binary file not shown.