Compare commits
74
Commits
main
...
2d1c4c9934
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d1c4c9934 | ||
|
|
29ba93e096 | ||
|
|
09862a53c1 | ||
|
|
336f1fd466 | ||
|
|
2937ba4344 | ||
|
|
52eed8aade | ||
|
|
6acf547df1 | ||
|
|
d7f3a7ef06 | ||
|
|
eccd940727 | ||
|
|
52eaeaae93 | ||
|
|
c99279cb49 | ||
|
|
2d9c6ffb1d | ||
|
|
240791d22e | ||
|
|
86e0b4a23f | ||
|
|
1f5d99b5ae | ||
|
|
cd0bfd956c | ||
|
|
6cf28240df | ||
|
|
697ac311a7 | ||
|
|
d08f207fc4 | ||
|
|
74fa6432db | ||
|
|
8ab282eb95 | ||
|
|
3d18c01d5a | ||
|
|
2404692a30 | ||
|
|
557f80979b | ||
|
|
fac4a27b75 | ||
|
|
2d80a76353 | ||
|
|
d5c36e3118 | ||
|
|
ff19c2faae | ||
|
|
4a7e8ef7ca | ||
|
|
3f2b208651 | ||
|
|
fa334f90d4 | ||
|
|
43e535143b | ||
|
|
f0ded8ac17 | ||
|
|
f78c8a6cd7 | ||
|
|
cbcbc45d00 | ||
|
|
869faf720a | ||
|
|
5d39cf3172 | ||
|
|
137b13d94e | ||
|
|
b1008ec425 | ||
|
|
22de950cf5 | ||
|
|
fd0a9a83e8 | ||
|
|
79302e8139 | ||
|
|
1ca4807d4b | ||
|
|
4c086cfad5 | ||
|
|
b91ea08874 | ||
|
|
ac69a7a5c6 | ||
|
|
0a576b7ec0 | ||
|
|
e46f78b971 | ||
|
|
a8b0fca263 | ||
|
|
bbb2e0f024 | ||
|
|
fbf38991d7 | ||
|
|
f672517cc8 | ||
|
|
03a1b243bc | ||
|
|
de745d74dc | ||
|
|
9c49b131e2 | ||
|
|
0912c0a253 | ||
|
|
e2a1e53082 | ||
|
|
80d9befa6a | ||
|
|
fcf5753f57 | ||
|
|
413694f47d | ||
|
|
2e912c7ad5 | ||
|
|
755e736ba9 | ||
|
|
12f3c65c39 | ||
|
|
02b908c4cd | ||
|
|
d059b86dfc | ||
|
|
6e3814ce3f | ||
|
|
fe24fec3a3 | ||
|
|
7d51740a1a | ||
|
|
ca01e8f17e | ||
|
|
b7cf1499a7 | ||
|
|
63db96d1bd | ||
|
|
e1b7a88d1f | ||
|
|
aaf111919d | ||
|
|
f35bd094c7 |
@@ -0,0 +1,4 @@
|
||||
---
|
||||
collections:
|
||||
- name: community.proxmox
|
||||
version: "2.0.0"
|
||||
@@ -0,0 +1,432 @@
|
||||
---
|
||||
# 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_msg != "Firmware status requires to check for update first to provide more information."
|
||||
retries: 10
|
||||
delay: 2
|
||||
|
||||
- name: Store node facts
|
||||
ansible.builtin.set_fact:
|
||||
firmware_action: "{{ opnsense_action | default('update')}}"
|
||||
firmware_status: "{{ _firmware_status.json.status }}"
|
||||
firmware_status_msg: "{{ _firmware_status.json.status_msg }}"
|
||||
firmware_current_version: "{{ _firmware_status.json.product.product_version | default('unknown') }}"
|
||||
firmware_product_series: "{{ _firmware_status.json.product.product_series | default('unknown') }}"
|
||||
firmware_update_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('') }}"
|
||||
firmware_up_to_date: "{{ _firmware_status.json.status_msg == 'There are no updates available on the selected mirror.' }}"
|
||||
needs_reboot: "{{ (_firmware_status.json.upgrade_needs_reboot == '1') if _firmware_status.json.status == 'upgrade' else (_firmware_status.json.needs_reboot == '1') }}"
|
||||
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: Resolve update-vs-upgrade specifics
|
||||
ansible.builtin.set_fact:
|
||||
firmware_target_kind: "{{ 'series' if firmware_status == 'upgrade' else 'version' }}"
|
||||
firmware_target_value: "{{ firmware_upgrade_version if firmware_status == 'upgrade' else firmware_update_version }}"
|
||||
|
||||
- name: Backup already updated
|
||||
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
|
||||
if hostvars[groups['opnsense_master'][0]].firmware_target_kind == 'version'
|
||||
else hostvars[groups['opnsense_backup'][0]].firmware_product_series)
|
||||
== hostvars[groups['opnsense_master'][0]].firmware_target_value
|
||||
|
||||
- name: Skip node {{ firmware_action }}
|
||||
ansible.builtin.set_fact:
|
||||
skip_update: true
|
||||
firmware_status: "skipped"
|
||||
when: firmware_up_to_date or firmware_action != firmware_status
|
||||
|
||||
- name: "Assert"
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- 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 in_maintenance %}
|
||||
The node {{ inventory_hostname }} is in CARP maintenance mode.
|
||||
{% elif mismatched_vips > 0 %}
|
||||
Some VIP are not {{ main_role }} on {{ ansible_host }}.
|
||||
{% elif total_vips == 0 %}
|
||||
No VIPs are managed.
|
||||
{% else %}
|
||||
An error occured during the {{ firmware_action }}.
|
||||
{% endif %}
|
||||
headers:
|
||||
Title: "OPNsense {{ firmware_action }} report"
|
||||
Priority: "default"
|
||||
Tags: "x"
|
||||
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
|
||||
until: _update_running.json.status == 'busy'
|
||||
retries: 2
|
||||
delay: 2
|
||||
|
||||
- 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: 5400
|
||||
delay: 30
|
||||
when: needs_reboot
|
||||
|
||||
- name: Check firmware version
|
||||
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_firmware_status
|
||||
until: _post_firmware_status.json.product['product_' ~ firmware_target_kind] | default('unknown') == firmware_target_value
|
||||
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: 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_target_value }} 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"
|
||||
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:
|
||||
|
||||
- when: not skip_update | default(false)
|
||||
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_target_value }}"
|
||||
|
||||
- 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
|
||||
until: _update_running.json.status == 'busy'
|
||||
retries: 2
|
||||
delay: 2
|
||||
|
||||
- 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: 5400
|
||||
delay: 30
|
||||
when: needs_reboot
|
||||
|
||||
- name: Check firmware version
|
||||
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_firmware_status
|
||||
until: _post_firmware_status.json.product['product_' ~ firmware_target_kind] | default('unknown') == firmware_target_value
|
||||
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_target_value }} failed on {{ inventory_hostname }} ({{ main_role }}).
|
||||
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"
|
||||
when: ntfy_url is defined
|
||||
|
||||
- ansible.builtin.fail:
|
||||
msg: "Update aborted"
|
||||
|
||||
|
||||
- name: "Phase 4 | Notification"
|
||||
hosts: localhost
|
||||
gather_facts: false
|
||||
tasks:
|
||||
|
||||
- name: Send Ntfy notification
|
||||
vars:
|
||||
master: "{{ groups['opnsense_master'][0] }}"
|
||||
backup: "{{ groups['opnsense_backup'][0] }}"
|
||||
m: "{{ hostvars[master] }}"
|
||||
b: "{{ hostvars[backup] }}"
|
||||
same_operation: "{{ m.firmware_status == b.firmware_status }}"
|
||||
|
||||
ansible.builtin.uri:
|
||||
url: "{{ ntfy_url }}/{{ ntfy_topic }}"
|
||||
method: POST
|
||||
user: "{{ ntfy_user }}"
|
||||
password: "{{ lookup('env', 'NTFY_PASSWORD') }}"
|
||||
force_basic_auth: true
|
||||
body: |
|
||||
{% if same_operation %}
|
||||
{% if m.skip_update | default(false) %}
|
||||
Both nodes already on {{ m.firmware_current_version }}, no action taken.
|
||||
{% else %}
|
||||
OPNsense cluster: {{ m.firmware_current_version }} → {{ m.firmware_target_value }} ({{ m.firmware_status }})
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if m.skip_update | default(false) %}
|
||||
MASTER ({{ master }}): already on {{ m.firmware_current_version }}, no action taken.
|
||||
{% else %}
|
||||
MASTER ({{ master }}): {{ m.firmware_current_version }} → {{ m.firmware_target_value }} ({{ m.firmware_status }})
|
||||
{% endif %}
|
||||
{% if b.skip_update | default(false) %}
|
||||
BACKUP ({{ backup }}): already on {{ b.firmware_current_version }}, no action taken.
|
||||
{% else %}
|
||||
BACKUP ({{ backup }}): {{ b.firmware_current_version }} → {{ b.firmware_target_value }} ({{ b.firmware_status }})
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
headers:
|
||||
Title: >-
|
||||
{%- if m.skip_update | default(false) and b.skip_update | default(false) -%}
|
||||
OPNsense cluster - no {{m.firmware_action }} available
|
||||
{%- else -%}
|
||||
OPNsense cluster {{m.firmware_action }} completed
|
||||
{%- endif -%}
|
||||
Priority: "{{ 'min' if (m.skip_update | default(false) and b.skip_update | default(false)) else 'default' }}"
|
||||
Tags: "{{ 'information_source' if (m.skip_update | default(false) and b.skip_update | default(false)) else 'white_check_mark' }}"
|
||||
when: ntfy_url is defined
|
||||
+3
-3
@@ -84,7 +84,7 @@ if ($type === "MASTER") {
|
||||
write_config("disable interface '$ifkey' due CARP event '$type'", false);
|
||||
interface_configure(false, $ifkey, false, false);
|
||||
// Remove WAN default gateway
|
||||
mwexec("/sbin/route delete default");
|
||||
mwexecf("/sbin/route delete default");
|
||||
foreach ($config['OPNsense']['Gateways']['gateway_item'] as $gw) {
|
||||
if ($gw['name'] === $gw_name) {
|
||||
$gw_ip = $gw['gateway'];
|
||||
@@ -92,9 +92,9 @@ if ($type === "MASTER") {
|
||||
}
|
||||
}
|
||||
// Shutdown WAN interface
|
||||
mwexec("/sbin/ifconfig {$real_if} down")
|
||||
mwexecf("/sbin/ifconfig {$real_if} down")
|
||||
// Add fallback default gateway
|
||||
mwexec("/sbin/route add default {$gw_ip}");
|
||||
mwexecf("/sbin/route add default {$gw_ip}");
|
||||
// Create lock file
|
||||
file_put_contents($lock_file, time());
|
||||
sleep($lock_default_age);
|
||||
|
||||
Reference in New Issue
Block a user