Compare commits

...

25 Commits

Author SHA1 Message Date
2918ea4578 add: requirements.yml for community.proxmox 2026-06-21 19:59:49 +00:00
c959176ebd add: master update with Proxmox snapshoty 2026-06-21 19:59:27 +00:00
3d6511262e add: skip backuo 2026-06-19 20:18:54 +00:00
a30bbddbf5 add: firmware update trigger 2026-06-19 12:51:08 +00:00
49a50e5af3 fix: use inventory_hostname 2026-06-19 12:40:30 +00:00
30f53ba20a fix: user ansible_inventory instead of host 2026-06-19 12:38:36 +00:00
c1b9ea7e51 add: ntfy notification for pre-check 2026-06-19 12:35:42 +00:00
51e7886131 add: changed_when 2026-06-18 20:46:24 +00:00
72c13366e1 add: register when enable maintenance 2026-06-18 20:42:14 +00:00
70fbe3ba0a fix: variable 2026-06-18 20:36:22 +00:00
8d76623e3a add: start phase 2 2026-06-18 20:33:10 +00:00
fbcbab5040 test: inventory var 2026-06-18 18:52:33 +00:00
0649dbdf6f add: get vip status 2026-06-18 18:47:34 +00:00
a24575785b add: reboot info 2026-06-18 18:47:34 +00:00
b168490adb fix: typo 2026-06-18 18:47:34 +00:00
0c5f5f866d fix: variables 2026-06-18 18:47:34 +00:00
e242e7c945 fix: var name 2026-06-18 18:47:34 +00:00
6e85085a9a fix: host variable for API url 2026-06-18 18:47:34 +00:00
d660f9f73e add: firmware check 2026-06-18 18:47:34 +00:00
8169f3023b Revert "test: unset validate certs"
This reverts commit 3c8375d6f4.
2026-06-18 18:47:34 +00:00
4e2dd232d3 test: unset validate certs 2026-06-18 18:47:34 +00:00
b383ef6f42 fix: remove port 2026-06-18 18:47:34 +00:00
847e2a3c6b test: removed vars 2026-06-18 18:47:34 +00:00
5dee978c28 test: validate_certs to false 2026-06-18 18:47:34 +00:00
1dcf77a85f add: OPNsense HA cluster update playbook 2026-06-18 18:47:34 +00:00
3 changed files with 396 additions and 1 deletions

View File

@@ -17,3 +17,4 @@ all:
ansible_host: 192.168.88.2
main_role: MASTER
hypervisor: Proxmox
proxmox_vmid: 122

View File

@@ -0,0 +1,4 @@
---
collections:
- name: community.proxmox
version: "2.0.0"

View File

@@ -0,0 +1,390 @@
---
# OPNsense HA Cluster Update
# Order: backup (TrueNAS) first, then master (Proxmox)
###############################################################
- name: "Phase 1 | Firmware check on all nodes"
hosts: opnsense
any_errors_fatal: true
gather_facts: false
tasks:
- block:
- name: Check node availability
ansible.builtin.uri:
url: "https://{{ opnsense_host }}/api/core/system/status"
method: GET
user: "{{ opnsense_api_key }}"
password: "{{ opnsense_api_secret }}"
force_basic_auth: true
validate_certs: false
- name: Trigger firmware check
ansible.builtin.uri:
url: "https://{{ opnsense_host }}/api/core/firmware/check"
method: POST
user: "{{ opnsense_api_key }}"
password: "{{ opnsense_api_secret }}"
force_basic_auth: true
validate_certs: false
- name: Check VIP status
ansible.builtin.uri:
url: "https://{{ opnsense_host }}/api/diagnostics/interface/get_vip_status"
method: GET
user: "{{ opnsense_api_key }}"
password: "{{ opnsense_api_secret }}"
force_basic_auth: true
validate_certs: false
register: _vip_status
- name: Wait for check to complete
ansible.builtin.uri:
url: "https://{{ opnsense_host }}/api/core/firmware/status"
method: GET
user: "{{ opnsense_api_key }}"
password: "{{ opnsense_api_secret }}"
force_basic_auth: true
validate_certs: false
register: _firmware_status
until: _firmware_status.json.status != 'none'
retries: 12
delay: 5
- name: Store firmware facts
ansible.builtin.set_fact:
firmware_status: "{{ _firmware_status.json.status }}"
firmware_current_version: "{{ _firmware_status.json.product_version | default('unknown') }}"
firmware_new_version: "{{ _firmware_status.json.upgrade_packages | selectattr('name', 'equalto', 'opnsense') | map(attribute='new_version') | first | default('') }}"
firmware_upgrade_version: "{{ _firmware_status.json.upgrade_major_version | default('unknown') }}"
needs_reboot: "{{ true if _firmware_status.json.needs_reboot == '1' else false }}"
total_vips: "{{ _vip_status.json.rowCount }}"
mismatched_vips: "{{ _vip_status.json.rows | rejectattr('status', 'equalto', main_role) | list | length }}"
in_maintenance: "{{ _vip_status.json.carp.maintenancemode }}"
# update_result: "{{ 'pending' if _firmware_status.json.status == 'update' else 'skipped' }}"
- name: Skip backup update
ansible.builtin.set_fact:
skip_update: true
delegate_to: "{{ groups['opnsense_backup'][0] }}"
delegate_facts: true
run_once: true
when: hostvars[groups['opnsense_backup'][0]].firmware_current_version == hostvars[groups['opnsense_master'][0]].firmware_new_version
- name: "Assert"
ansible.builtin.assert:
that:
- firmware_status == 'update' or skip_update
- not in_maintenance
- mismatched_vips == 0
- total_vips > 0
quiet: true
- name: Display firmware status
ansible.builtin.debug:
msg: >-
{{ inventory_hostname }} ({{ main_role }}): {{ firmware_status }}
({{ firmware_current_version }}
{% if firmware_new_version %} → {{ firmware_new_version }} (reboot: {{ needs_reboot }}){% endif %})
rescue:
- name: Send Ntfy notification
ansible.builtin.uri:
url: "{{ ntfy_url }}/{{ ntfy_topic }}"
method: POST
user: "{{ ntfy_user }}"
password: "{{ lookup('env', 'NTFY_PASSWORD') }}"
force_basic_auth: true
body: |
Current version: {{ firmware_current_version }}
{% if firmware_status != "update" %}
{% if firmware_status == "upgrade" %}
Upgrade to {{ firmware_upgrade_version }} available, relaunch the playbook manually to proceed.
{% else %}
No updates available on the cluster.
{% endif %}
{% elif in_maintenance %}
The node {{ inventory_hostname }} is in CARP maintenance mode.
{% elif mismatched_vips > 0 %}
Some VIP are not {{ main_role }} on {{ ansible_host }}.
{% else %}
No VIPs are managed.
{% endif %}
headers:
Title: "OPNsense update aborted"
Priority: "default"
Tags: "x"
run_once: true
when: ntfy_url is defined
- ansible.builtin.fail:
msg: "Update aborted"
- name: "Phase 2 | Update backup node (TrueNAS, no snapshot)"
hosts: opnsense_backup
gather_facts: false
tasks:
- when: not skip_update | default(false)
block:
- name: Enable CARP maintenance mode
ansible.builtin.uri:
url: "https://{{ opnsense_host }}/api/diagnostics/interface/carp_status/maintenance"
method: POST
user: "{{ opnsense_api_key }}"
password: "{{ opnsense_api_secret }}"
force_basic_auth: true
validate_certs: false
register: _enable_maintenance
changed_when: _enable_maintenance.status == "ok"
- name: Trigger firmware update
ansible.builtin.uri:
url: "https://{{ opnsense_host }}/api/core/firmware/update"
method: POST
user: "{{ opnsense_api_key }}"
password: "{{ opnsense_api_secret }}"
force_basic_auth: true
validate_certs: false
- name: Wait for packages to update
ansible.builtin.uri:
url: "https://{{ opnsense_host }}/api/core/firmware/running"
method: GET
user: "{{ opnsense_api_key }}"
password: "{{ opnsense_api_secret }}"
force_basic_auth: true
validate_certs: false
register: _update_running
until: _update_running.json.status != 'busy'
retries: 300
delay: 15
- name: Wait for node to go offline
ansible.builtin.wait_for:
host: "{{ ansible_host }}"
port: "{{ opnsense_https_port }}"
state: stopped
timeout: 60
when: needs_reboot
- name: Wait for node to come back online
ansible.builtin.wait_for:
host: "{{ ansible_host }}"
port: "{{ opnsense_https_port }}"
state: started
timeout: 300
delay: 30
when: needs_reboot
- name: Wait for API to be fully responsive
ansible.builtin.uri:
url: "https://{{ opnsense_host }}/api/core/firmware/status"
method: GET
user: "{{ opnsense_api_key }}"
password: "{{ opnsense_api_secret }}"
force_basic_auth: true
validate_certs: false
register: _post_status
until: _post_status.status == 200
retries: 20
delay: 10
- name: Check VIP status
ansible.builtin.uri:
url: "https://{{ opnsense_host }}/api/diagnostics/interface/get_vip_status"
method: GET
user: "{{ opnsense_api_key }}"
password: "{{ opnsense_api_secret }}"
force_basic_auth: true
validate_certs: false
register: _carp_post_udapte
- name: Disable CARP maintenance mode
ansible.builtin.uri:
url: "https://{{ opnsense_host }}/api/carp/status/maintenancemode"
method: POST
user: "{{ opnsense_api_key }}"
password: "{{ opnsense_api_secret }}"
force_basic_auth: true
validate_certs: false
when: _carp_post_udapte.json.carp.maintenancemode
rescue:
- name: Send Ntfy notification
ansible.builtin.uri:
url: "{{ ntfy_url }}/{{ ntfy_topic }}"
method: POST
user: "{{ ntfy_user }}"
password: "{{ lookup('env', 'NTFY_PASSWORD') }}"
force_basic_auth: true
body: |
TODO
# Current version: {{ firmware_current_version }}
# {% if firmware_status != "update" %}
# {% if firmware_status == "upgrade" %}
# Upgrade to {{ firmware_upgrade_version }} available, relaunch the playbook manually to proceed.
# {% else %}
# No updates available on the cluster.
# {% endif %}
# {% elif in_maintenance %}
# The node {{ inventory_hostname }} is in CARP maintenance mode.
# {% elif mismatched_vips > 0 %}
# Some VIP are not {{ main_role }} on {{ ansible_host }}.
# {% else %}
# No VIPs are managed.
# {% endif %}
headers:
Title: "OPNsense update failed on {{ inventory_hostname }}"
Priority: "default"
Tags: "x"
run_once: true
when: ntfy_url is defined
- ansible.builtin.fail:
msg: "Update aborted"
- name: "Phase 3 | Update master node (Proxmox, with snapshot)"
hosts: opnsense_master
gather_facts: false
vars:
proxmox_snap_name: "preupdate-{{ now().strftime('%Y%m%d-%H%M') }}"
tasks:
- block:
- name: Take Proxmox VM snapshot
community.general.proxmox_snap:
vmid: "{{ proxmox_vmid }}"
state: present
snapname: "{{ proxmox_snap_name }}"
description: "Pre-firmware-update: {{ firmware_current_version }} → {{ firmware_new_version }}"
- name: Enable CARP maintenance mode
ansible.builtin.uri:
url: "https://{{ opnsense_host }}/api/diagnostics/interface/carp_status/maintenance"
method: POST
user: "{{ opnsense_api_key }}"
password: "{{ opnsense_api_secret }}"
force_basic_auth: true
validate_certs: false
register: _enable_maintenance
changed_when: _enable_maintenance.status == "ok"
- name: Trigger firmware update
ansible.builtin.uri:
url: "https://{{ opnsense_host }}/api/core/firmware/update"
method: POST
user: "{{ opnsense_api_key }}"
password: "{{ opnsense_api_secret }}"
force_basic_auth: true
validate_certs: false
- name: Wait for packages to update
ansible.builtin.uri:
url: "https://{{ opnsense_host }}/api/core/firmware/running"
method: GET
user: "{{ opnsense_api_key }}"
password: "{{ opnsense_api_secret }}"
force_basic_auth: true
validate_certs: false
register: _update_running
until: _update_running.json.status != 'busy'
retries: 300
delay: 15
- name: Wait for node to go offline
ansible.builtin.wait_for:
host: "{{ ansible_host }}"
port: "{{ opnsense_https_port }}"
state: stopped
timeout: 60
when: needs_reboot
- name: Wait for node to come back online
ansible.builtin.wait_for:
host: "{{ ansible_host }}"
port: "{{ opnsense_https_port }}"
state: started
timeout: 300
delay: 30
when: needs_reboot
- name: Wait for API to be fully responsive
ansible.builtin.uri:
url: "https://{{ opnsense_host }}/api/core/firmware/status"
method: GET
user: "{{ opnsense_api_key }}"
password: "{{ opnsense_api_secret }}"
force_basic_auth: true
validate_certs: false
register: _post_status
until: _post_status.status == 200
retries: 20
delay: 10
- name: Check VIP status
ansible.builtin.uri:
url: "https://{{ opnsense_host }}/api/diagnostics/interface/get_vip_status"
method: GET
user: "{{ opnsense_api_key }}"
password: "{{ opnsense_api_secret }}"
force_basic_auth: true
validate_certs: false
register: _carp_post_udapte
- name: Disable CARP maintenance mode
ansible.builtin.uri:
url: "https://{{ opnsense_host }}/api/carp/status/maintenancemode"
method: POST
user: "{{ opnsense_api_key }}"
password: "{{ opnsense_api_secret }}"
force_basic_auth: true
validate_certs: false
when: _carp_post_udapte.json.carp.maintenancemode
rescue:
- name: Rollback VM to the pre-update snapshot
community.proxmox.proxmox_snap:
vmid: "{{ proxmox_vmid }}"
state: rollback
snapname: "{{ proxmox_snap_name }}"
- name: Send Ntfy notification
ansible.builtin.uri:
url: "{{ ntfy_url }}/{{ ntfy_topic }}"
method: POST
user: "{{ ntfy_user }}"
password: "{{ lookup('env', 'NTFY_PASSWORD') }}"
force_basic_auth: true
body: |
TODO
# Current version: {{ firmware_current_version }}
# {% if firmware_status != "update" %}
# {% if firmware_status == "upgrade" %}
# Upgrade to {{ firmware_upgrade_version }} available, relaunch the playbook manually to proceed.
# {% else %}
# No updates available on the cluster.
# {% endif %}
# {% elif in_maintenance %}
# The node {{ inventory_hostname }} is in CARP maintenance mode.
# {% elif mismatched_vips > 0 %}
# Some VIP are not {{ main_role }} on {{ ansible_host }}.
# {% else %}
# No VIPs are managed.
# {% endif %}
headers:
Title: "OPNsense update failed on {{ inventory_hostname }}"
Priority: "default"
Tags: "x"
run_once: true
when: ntfy_url is defined
- ansible.builtin.fail:
msg: "Update aborted"