399 lines
15 KiB
YAML
399 lines
15 KiB
YAML
---
|
|
# 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 node facts
|
|
ansible.builtin.set_fact:
|
|
firmware_action: "{{ opnsense_action | default('update')}}"
|
|
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') }}"
|
|
firmware_upgrade_message: "{{ _firmware_status.json.upgrade_major_message | regex_replace('<[^>]+>', ' ') | regex_replace('\s{2,}', ' ') | trim | default('') }}"
|
|
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 }}"
|
|
|
|
- name: Skip backup {{ firmware_action }}
|
|
ansible.builtin.set_fact:
|
|
skip_update: true
|
|
firmware_status: "skipped"
|
|
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_action == firmware_status or skip_update | default(false)
|
|
- not in_maintenance
|
|
- mismatched_vips == 0
|
|
- total_vips > 0
|
|
quiet: true
|
|
|
|
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_action != firmware_status %}
|
|
{% if firmware_status == "upgrade" %}
|
|
Upgrade to {{ firmware_upgrade_version }} available, but no upgrade requested.
|
|
{% if firmware_upgrade_message != "" %}
|
|
Upgrade message:
|
|
{{ firmware_upgrade_message }}
|
|
{% endif %}
|
|
{% 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 {{ firmware_action }} report"
|
|
Priority: "default"
|
|
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
|
|
any_errors_fatal: true
|
|
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 {{ firmware_action }}
|
|
ansible.builtin.uri:
|
|
url: "https://{{ opnsense_host }}/api/core/firmware/{{ firmware_action }}"
|
|
method: POST
|
|
user: "{{ opnsense_api_key }}"
|
|
password: "{{ opnsense_api_secret }}"
|
|
force_basic_auth: true
|
|
validate_certs: false
|
|
|
|
- name: Check if the {{ firmware_action }} is running
|
|
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
|
|
failed_when: _update_running.json.status != 'busy'
|
|
|
|
- name: Wait for node to reboot after the {{ firmware_action }}
|
|
ansible.builtin.wait_for:
|
|
host: "{{ ansible_host }}"
|
|
port: "{{ opnsense_https_port }}"
|
|
state: stopped
|
|
timeout: 3600
|
|
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: Check firmware version
|
|
ansible.builtin.uri:
|
|
url: "https://{{ opnsense_host }}/api/core/firmware/status"
|
|
method: POST
|
|
user: "{{ opnsense_api_key }}"
|
|
password: "{{ opnsense_api_secret }}"
|
|
force_basic_auth: true
|
|
validate_certs: false
|
|
register: _post_firmware_status
|
|
until: _post_firmware_status.json.product_version | default('unknown') == firmware_new_version
|
|
retries: 240
|
|
delay: 15
|
|
|
|
- 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
|
|
ignore_errors: true
|
|
|
|
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 }}
|
|
{{ firmware_action | capitalize }} to {{ firmware_new_version }} failed on {{ inventory_hostname }} ({{ main_role }}).
|
|
No snapshot have been taken before the {{ firmware_action }}.
|
|
headers:
|
|
Title: "OPNsense {{ firmware_action }} failed on {{ inventory_hostname }}"
|
|
Priority: "high"
|
|
Tags: "x"
|
|
run_once: true
|
|
when: ntfy_url is defined
|
|
|
|
- ansible.builtin.fail:
|
|
msg: "Update failed"
|
|
|
|
- name: "Phase 3 | Update master node (Proxmox, with snapshot)"
|
|
hosts: opnsense_master
|
|
any_errors_fatal: true
|
|
gather_facts: false
|
|
vars:
|
|
proxmox_snap_name: "preupdate-{{ now().strftime('%Y%m%d') }}"
|
|
|
|
tasks:
|
|
|
|
- block:
|
|
|
|
- name: Take Proxmox VM snapshot
|
|
community.proxmox.proxmox_snap:
|
|
vmid: "{{ proxmox_vmid }}"
|
|
state: present
|
|
snapname: "{{ proxmox_snap_name }}"
|
|
description: "Pre-firmware-{{ firmware_action }}: {{ 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: Wait for CARP failover to settle
|
|
ansible.builtin.pause:
|
|
seconds: 5
|
|
|
|
- name: Trigger firmware {{ firmware_action }}
|
|
ansible.builtin.uri:
|
|
url: "https://{{ opnsense_host }}/api/core/firmware/{{ firmware_action }}"
|
|
method: POST
|
|
user: "{{ opnsense_api_key }}"
|
|
password: "{{ opnsense_api_secret }}"
|
|
force_basic_auth: true
|
|
validate_certs: false
|
|
|
|
- name: Check if the {{ firmware_action }} is running
|
|
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
|
|
failed_when: _update_running.json.status != 'busy'
|
|
|
|
- name: Wait for node to reboot after the {{ firmware_action }}
|
|
ansible.builtin.wait_for:
|
|
host: "{{ ansible_host }}"
|
|
port: "{{ opnsense_https_port }}"
|
|
state: stopped
|
|
timeout: 3600
|
|
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: Check firmware version
|
|
ansible.builtin.uri:
|
|
url: "https://{{ opnsense_host }}/api/core/firmware/status"
|
|
method: POST
|
|
user: "{{ opnsense_api_key }}"
|
|
password: "{{ opnsense_api_secret }}"
|
|
force_basic_auth: true
|
|
validate_certs: false
|
|
register: _post_firmware_status
|
|
until: _post_firmware_status.json.product_version | default('unknown') == firmware_new_version
|
|
retries: 240
|
|
delay: 15
|
|
|
|
- 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/diagnostics/interface/carp_status/maintenance"
|
|
method: POST
|
|
user: "{{ opnsense_api_key }}"
|
|
password: "{{ opnsense_api_secret }}"
|
|
force_basic_auth: true
|
|
validate_certs: false
|
|
when: _carp_post_udapte.json.carp.maintenancemode
|
|
ignore_errors: true
|
|
|
|
rescue:
|
|
|
|
- name: Rollback VM to the pre-{{ firmware_action }} snapshot
|
|
community.proxmox.proxmox_snap:
|
|
vmid: "{{ proxmox_vmid }}"
|
|
state: rollback
|
|
snapname: "{{ proxmox_snap_name }}"
|
|
register: _proxmox_snapshot_rollback
|
|
ignore_errors: true
|
|
|
|
- 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 }}
|
|
{{ firmware_action | capitalize }} to {{ firmware_new_version }} failed on {{ inventory_hostname }} ({{ main_role }}).
|
|
# {% if }
|
|
A snapshot have been taken before the {{ firmware_action }} and the VM has been rollback.
|
|
headers:
|
|
Title: "OPNsense {{ firmware_action }} failed on {{ inventory_hostname }}"
|
|
Priority: "high"
|
|
Tags: "x"
|
|
run_once: true
|
|
when: ntfy_url is defined
|
|
|
|
- ansible.builtin.fail:
|
|
msg: "Update aborted"
|
|
|
|
|
|
- name: "Phase 4 | Notify on succes"
|
|
hosts: localhost
|
|
gather_facts: false
|
|
tasks:
|
|
|
|
- name: Send Ntfy notification
|
|
vars:
|
|
master: "{{ groups['opnsense_master'][0] }}"
|
|
backup: "{{ groups['opnsense_backup'][0] }}"
|
|
ansible.builtin.uri:
|
|
url: "{{ ntfy_url }}/{{ ntfy_topic }}"
|
|
method: POST
|
|
user: "{{ ntfy_user }}"
|
|
password: "{{ lookup('env', 'NTFY_PASSWORD') }}"
|
|
force_basic_auth: true
|
|
body: |
|
|
OPNsense HA cluster {{ firmware_action }} completed successfully.
|
|
{% for host in [master, backup] %}
|
|
{% set h = hostvars[host] %}
|
|
{{ h.main_role }} ({{ host }}): {% if h.skip_update %}already on {{ h.firmware_current_version }}, skipped{% else %}{{ h.firmware_current_version }} → {{ h.firmware_new_version }}{% endif %}{{ "\n" if not loop.last else "" }}
|
|
{% endfor %}
|
|
headers:
|
|
Title: "OPNsense {{ firmware_action }} completed"
|
|
Priority: "default"
|
|
Tags: "white_check_mark"
|
|
when: ntfy_url is defined |