|
|
|
@@ -47,58 +47,46 @@
|
|
|
|
force_basic_auth: true
|
|
|
|
force_basic_auth: true
|
|
|
|
validate_certs: false
|
|
|
|
validate_certs: false
|
|
|
|
register: _firmware_status
|
|
|
|
register: _firmware_status
|
|
|
|
until: _firmware_status.json.status_msg != "Firmware status requires to check for update first to provide more information."
|
|
|
|
until: _firmware_status.json.status != 'none'
|
|
|
|
retries: 10
|
|
|
|
retries: 12
|
|
|
|
delay: 2
|
|
|
|
delay: 5
|
|
|
|
|
|
|
|
|
|
|
|
- name: Store node facts
|
|
|
|
- name: Store firmware facts
|
|
|
|
ansible.builtin.set_fact:
|
|
|
|
ansible.builtin.set_fact:
|
|
|
|
firmware_action: "{{ opnsense_action | default('update')}}"
|
|
|
|
|
|
|
|
firmware_status: "{{ _firmware_status.json.status }}"
|
|
|
|
firmware_status: "{{ _firmware_status.json.status }}"
|
|
|
|
firmware_status_msg: "{{ _firmware_status.json.status_msg }}"
|
|
|
|
firmware_current_version: "{{ _firmware_status.json.product_version | default('unknown') }}"
|
|
|
|
firmware_current_version: "{{ _firmware_status.json.product.product_version | default('unknown') }}"
|
|
|
|
firmware_new_version: "{{ _firmware_status.json.upgrade_packages | selectattr('name', 'equalto', 'opnsense') | map(attribute='new_version') | first | default('') }}"
|
|
|
|
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_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 }}"
|
|
|
|
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 }}"
|
|
|
|
total_vips: "{{ _vip_status.json.rowCount }}"
|
|
|
|
mismatched_vips: "{{ _vip_status.json.rows | rejectattr('status', 'equalto', main_role) | list | length }}"
|
|
|
|
mismatched_vips: "{{ _vip_status.json.rows | rejectattr('status', 'equalto', main_role) | list | length }}"
|
|
|
|
in_maintenance: "{{ _vip_status.json.carp.maintenancemode }}"
|
|
|
|
in_maintenance: "{{ _vip_status.json.carp.maintenancemode }}"
|
|
|
|
|
|
|
|
# update_result: "{{ 'pending' if _firmware_status.json.status == 'update' else 'skipped' }}"
|
|
|
|
|
|
|
|
|
|
|
|
- name: Resolve update-vs-upgrade specifics
|
|
|
|
- name: Skip backup update
|
|
|
|
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:
|
|
|
|
ansible.builtin.set_fact:
|
|
|
|
skip_update: true
|
|
|
|
skip_update: true
|
|
|
|
firmware_status: "skipped"
|
|
|
|
|
|
|
|
delegate_to: "{{ groups['opnsense_backup'][0] }}"
|
|
|
|
delegate_to: "{{ groups['opnsense_backup'][0] }}"
|
|
|
|
delegate_facts: true
|
|
|
|
delegate_facts: true
|
|
|
|
run_once: true
|
|
|
|
run_once: true
|
|
|
|
when: >-
|
|
|
|
when: hostvars[groups['opnsense_backup'][0]].firmware_current_version == hostvars[groups['opnsense_master'][0]].firmware_new_version
|
|
|
|
(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"
|
|
|
|
- name: "Assert"
|
|
|
|
ansible.builtin.assert:
|
|
|
|
ansible.builtin.assert:
|
|
|
|
that:
|
|
|
|
that:
|
|
|
|
|
|
|
|
- firmware_status == 'update'
|
|
|
|
- not in_maintenance
|
|
|
|
- not in_maintenance
|
|
|
|
- mismatched_vips == 0
|
|
|
|
- mismatched_vips == 0
|
|
|
|
- total_vips > 0
|
|
|
|
- total_vips > 0
|
|
|
|
quiet: true
|
|
|
|
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:
|
|
|
|
rescue:
|
|
|
|
|
|
|
|
|
|
|
|
- name: Send Ntfy notification
|
|
|
|
- name: Send Ntfy notification
|
|
|
|
@@ -110,19 +98,24 @@
|
|
|
|
force_basic_auth: true
|
|
|
|
force_basic_auth: true
|
|
|
|
body: |
|
|
|
|
body: |
|
|
|
|
Current version: {{ firmware_current_version }}
|
|
|
|
Current version: {{ firmware_current_version }}
|
|
|
|
{% if in_maintenance %}
|
|
|
|
{% 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.
|
|
|
|
The node {{ inventory_hostname }} is in CARP maintenance mode.
|
|
|
|
{% elif mismatched_vips > 0 %}
|
|
|
|
{% elif mismatched_vips > 0 %}
|
|
|
|
Some VIP are not {{ main_role }} on {{ ansible_host }}.
|
|
|
|
Some VIP are not {{ main_role }} on {{ ansible_host }}.
|
|
|
|
{% elif total_vips == 0 %}
|
|
|
|
|
|
|
|
No VIPs are managed.
|
|
|
|
|
|
|
|
{% else %}
|
|
|
|
{% else %}
|
|
|
|
An error occured during the {{ firmware_action }}.
|
|
|
|
No VIPs are managed.
|
|
|
|
{% endif %}
|
|
|
|
{% endif %}
|
|
|
|
headers:
|
|
|
|
headers:
|
|
|
|
Title: "OPNsense {{ firmware_action }} report"
|
|
|
|
Title: "OPNsense update aborted"
|
|
|
|
Priority: "default"
|
|
|
|
Priority: "default"
|
|
|
|
Tags: "x"
|
|
|
|
Tags: "x"
|
|
|
|
|
|
|
|
run_once: true
|
|
|
|
when: ntfy_url is defined
|
|
|
|
when: ntfy_url is defined
|
|
|
|
|
|
|
|
|
|
|
|
- ansible.builtin.fail:
|
|
|
|
- ansible.builtin.fail:
|
|
|
|
@@ -131,12 +124,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
- name: "Phase 2 | Update backup node (TrueNAS, no snapshot)"
|
|
|
|
- name: "Phase 2 | Update backup node (TrueNAS, no snapshot)"
|
|
|
|
hosts: opnsense_backup
|
|
|
|
hosts: opnsense_backup
|
|
|
|
any_errors_fatal: true
|
|
|
|
|
|
|
|
gather_facts: false
|
|
|
|
gather_facts: false
|
|
|
|
tasks:
|
|
|
|
tasks:
|
|
|
|
|
|
|
|
|
|
|
|
- when: not skip_update | default(false)
|
|
|
|
- block:
|
|
|
|
block:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- name: Enable CARP maintenance mode
|
|
|
|
- name: Enable CARP maintenance mode
|
|
|
|
ansible.builtin.uri:
|
|
|
|
ansible.builtin.uri:
|
|
|
|
@@ -149,16 +140,16 @@
|
|
|
|
register: _enable_maintenance
|
|
|
|
register: _enable_maintenance
|
|
|
|
changed_when: _enable_maintenance.status == "ok"
|
|
|
|
changed_when: _enable_maintenance.status == "ok"
|
|
|
|
|
|
|
|
|
|
|
|
- name: Trigger firmware {{ firmware_action }}
|
|
|
|
- name: Trigger firmware update
|
|
|
|
ansible.builtin.uri:
|
|
|
|
ansible.builtin.uri:
|
|
|
|
url: "https://{{ opnsense_host }}/api/core/firmware/{{ firmware_action }}"
|
|
|
|
url: "https://{{ opnsense_host }}/api/core/firmware/update"
|
|
|
|
method: POST
|
|
|
|
method: POST
|
|
|
|
user: "{{ opnsense_api_key }}"
|
|
|
|
user: "{{ opnsense_api_key }}"
|
|
|
|
password: "{{ opnsense_api_secret }}"
|
|
|
|
password: "{{ opnsense_api_secret }}"
|
|
|
|
force_basic_auth: true
|
|
|
|
force_basic_auth: true
|
|
|
|
validate_certs: false
|
|
|
|
validate_certs: false
|
|
|
|
|
|
|
|
|
|
|
|
- name: Check if the {{ firmware_action }} is running
|
|
|
|
- name: Wait for packages to update
|
|
|
|
ansible.builtin.uri:
|
|
|
|
ansible.builtin.uri:
|
|
|
|
url: "https://{{ opnsense_host }}/api/core/firmware/running"
|
|
|
|
url: "https://{{ opnsense_host }}/api/core/firmware/running"
|
|
|
|
method: GET
|
|
|
|
method: GET
|
|
|
|
@@ -167,266 +158,357 @@
|
|
|
|
force_basic_auth: true
|
|
|
|
force_basic_auth: true
|
|
|
|
validate_certs: false
|
|
|
|
validate_certs: false
|
|
|
|
register: _update_running
|
|
|
|
register: _update_running
|
|
|
|
until: _update_running.json.status == 'busy'
|
|
|
|
until: _update_running.json.status != 'busy'
|
|
|
|
retries: 2
|
|
|
|
retries: 300
|
|
|
|
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
|
|
|
|
delay: 15
|
|
|
|
|
|
|
|
|
|
|
|
- name: Check VIP status
|
|
|
|
# - name: Wait for node to go offline
|
|
|
|
ansible.builtin.uri:
|
|
|
|
# ansible.builtin.wait_for:
|
|
|
|
url: "https://{{ opnsense_host }}/api/diagnostics/interface/get_vip_status"
|
|
|
|
# host: "{{ ansible_host }}"
|
|
|
|
method: GET
|
|
|
|
# port: 443
|
|
|
|
user: "{{ opnsense_api_key }}"
|
|
|
|
# state: stopped
|
|
|
|
password: "{{ opnsense_api_secret }}"
|
|
|
|
# timeout: 60
|
|
|
|
force_basic_auth: true
|
|
|
|
# delegate_to: localhost
|
|
|
|
validate_certs: false
|
|
|
|
|
|
|
|
register: _carp_post_udapte
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- name: Disable CARP maintenance mode
|
|
|
|
# - name: Wait for node to come back online
|
|
|
|
ansible.builtin.uri:
|
|
|
|
# ansible.builtin.wait_for:
|
|
|
|
url: "https://{{ opnsense_host }}/api/diagnostics/interface/carp_status/maintenance"
|
|
|
|
# host: "{{ ansible_host }}"
|
|
|
|
method: POST
|
|
|
|
# port: 443
|
|
|
|
user: "{{ opnsense_api_key }}"
|
|
|
|
# state: started
|
|
|
|
password: "{{ opnsense_api_secret }}"
|
|
|
|
# timeout: 300
|
|
|
|
force_basic_auth: true
|
|
|
|
# delay: 30
|
|
|
|
validate_certs: false
|
|
|
|
# delegate_to: localhost
|
|
|
|
when: _carp_post_udapte.json.carp.maintenancemode
|
|
|
|
|
|
|
|
ignore_errors: true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
rescue:
|
|
|
|
# - 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: Send Ntfy notification
|
|
|
|
# - name: Health check — no remaining updates
|
|
|
|
ansible.builtin.uri:
|
|
|
|
# ansible.builtin.assert:
|
|
|
|
url: "{{ ntfy_url }}/{{ ntfy_topic }}"
|
|
|
|
# that:
|
|
|
|
method: POST
|
|
|
|
# - _post_status.json.status == 'none'
|
|
|
|
user: "{{ ntfy_user }}"
|
|
|
|
# fail_msg: >-
|
|
|
|
password: "{{ lookup('env', 'NTFY_PASSWORD') }}"
|
|
|
|
# Post-update firmware status is '{{ _post_status.json.status }}'
|
|
|
|
force_basic_auth: true
|
|
|
|
# on {{ inventory_hostname }}. Expected 'none'.
|
|
|
|
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:
|
|
|
|
# - name: Health check — CARP role is BACKUP
|
|
|
|
msg: "Update failed"
|
|
|
|
# ansible.builtin.uri:
|
|
|
|
|
|
|
|
# url: "https://{{ opnsense_host }}/api/carp/status/get"
|
|
|
|
|
|
|
|
# method: GET
|
|
|
|
|
|
|
|
# user: "{{ opnsense_api_key }}"
|
|
|
|
|
|
|
|
# password: "{{ opnsense_api_secret }}"
|
|
|
|
|
|
|
|
# force_basic_auth: true
|
|
|
|
|
|
|
|
# validate_certs: false
|
|
|
|
|
|
|
|
# register: _carp_post
|
|
|
|
|
|
|
|
# failed_when: _carp_post.json.status | upper not in ['BACKUP', 'MAINTENANCE']
|
|
|
|
|
|
|
|
|
|
|
|
- name: "Phase 3 | Update master node (Proxmox, with snapshot)"
|
|
|
|
# - name: Disable CARP maintenance mode
|
|
|
|
hosts: opnsense_master
|
|
|
|
# ansible.builtin.uri:
|
|
|
|
any_errors_fatal: true
|
|
|
|
# url: "https://{{ opnsense_host }}/api/carp/status/maintenancemode"
|
|
|
|
gather_facts: false
|
|
|
|
# method: POST
|
|
|
|
vars:
|
|
|
|
# user: "{{ opnsense_api_key }}"
|
|
|
|
proxmox_snap_name: "preupdate-{{ now().strftime('%Y%m%d') }}"
|
|
|
|
# password: "{{ opnsense_api_secret }}"
|
|
|
|
|
|
|
|
# force_basic_auth: true
|
|
|
|
|
|
|
|
# validate_certs: false
|
|
|
|
|
|
|
|
# body_format: json
|
|
|
|
|
|
|
|
# body: {}
|
|
|
|
|
|
|
|
|
|
|
|
tasks:
|
|
|
|
# - name: Record backup update result
|
|
|
|
|
|
|
|
# ansible.builtin.set_fact:
|
|
|
|
- when: not skip_update | default(false)
|
|
|
|
# update_result: "success"
|
|
|
|
block:
|
|
|
|
# fw_post_version: "{{ _post_status.json.product_version | default('unknown') }}"
|
|
|
|
|
|
|
|
|
|
|
|
- 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
|
|
|
|
# - name: "Phase 3 | Update master node (Proxmox, with snapshot)"
|
|
|
|
gather_facts: false
|
|
|
|
# hosts: opnsense_master
|
|
|
|
tasks:
|
|
|
|
# gather_facts: false
|
|
|
|
|
|
|
|
|
|
|
|
- name: Send Ntfy notification
|
|
|
|
# vars:
|
|
|
|
vars:
|
|
|
|
# snap_name: "preupdate-{{ now().strftime('%Y%m%d-%H%M') }}"
|
|
|
|
master: "{{ groups['opnsense_master'][0] }}"
|
|
|
|
# _pve_base: "https://{{ proxmox_api_host }}/api2/json/nodes/{{ proxmox_node }}/qemu/{{ proxmox_vmid }}"
|
|
|
|
backup: "{{ groups['opnsense_backup'][0] }}"
|
|
|
|
# _pve_auth:
|
|
|
|
m: "{{ hostvars[master] }}"
|
|
|
|
# Authorization: "PVEAPIToken={{ proxmox_api_token_id }}={{ proxmox_api_token_secret }}"
|
|
|
|
b: "{{ hostvars[backup] }}"
|
|
|
|
|
|
|
|
same_operation: "{{ m.firmware_status == b.firmware_status }}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ansible.builtin.uri:
|
|
|
|
# tasks:
|
|
|
|
url: "{{ ntfy_url }}/{{ ntfy_topic }}"
|
|
|
|
# - name: Skip — no minor updates available on master
|
|
|
|
method: POST
|
|
|
|
# ansible.builtin.meta: end_host
|
|
|
|
user: "{{ ntfy_user }}"
|
|
|
|
# when: firmware_status != 'update'
|
|
|
|
password: "{{ lookup('env', 'NTFY_PASSWORD') }}"
|
|
|
|
|
|
|
|
force_basic_auth: true
|
|
|
|
# - name: Verify master node CARP role
|
|
|
|
body: |
|
|
|
|
# ansible.builtin.uri:
|
|
|
|
{% if same_operation %}
|
|
|
|
# url: "https://{{ opnsense_host }}/api/carp/status/get"
|
|
|
|
{% if m.skip_update | default(false) %}
|
|
|
|
# method: GET
|
|
|
|
Both nodes already on {{ m.firmware_current_version }}, no action taken.
|
|
|
|
# user: "{{ opnsense_api_key }}"
|
|
|
|
{% else %}
|
|
|
|
# password: "{{ opnsense_api_secret }}"
|
|
|
|
OPNsense cluster: {{ m.firmware_current_version }} → {{ m.firmware_target_value }} ({{ m.firmware_status }})
|
|
|
|
# force_basic_auth: true
|
|
|
|
{% endif %}
|
|
|
|
# validate_certs: false
|
|
|
|
{% else %}
|
|
|
|
# register: _carp
|
|
|
|
{% if m.skip_update | default(false) %}
|
|
|
|
# failed_when: _carp.json.status | upper != 'MASTER'
|
|
|
|
MASTER ({{ master }}): already on {{ m.firmware_current_version }}, no action taken.
|
|
|
|
|
|
|
|
{% else %}
|
|
|
|
# # Snapshot BEFORE maintenance mode — the restore point should be
|
|
|
|
MASTER ({{ master }}): {{ m.firmware_current_version }} → {{ m.firmware_target_value }} ({{ m.firmware_status }})
|
|
|
|
# # a fully functioning MASTER node, not one mid-failover
|
|
|
|
{% endif %}
|
|
|
|
# - name: Take Proxmox VM snapshot
|
|
|
|
{% if b.skip_update | default(false) %}
|
|
|
|
# community.general.proxmox_snap:
|
|
|
|
BACKUP ({{ backup }}): already on {{ b.firmware_current_version }}, no action taken.
|
|
|
|
# api_host: "{{ proxmox_api_host }}"
|
|
|
|
{% else %}
|
|
|
|
# api_token_id: "{{ proxmox_api_token_id }}"
|
|
|
|
BACKUP ({{ backup }}): {{ b.firmware_current_version }} → {{ b.firmware_target_value }} ({{ b.firmware_status }})
|
|
|
|
# api_token_secret: "{{ proxmox_api_token_secret }}"
|
|
|
|
{% endif %}
|
|
|
|
# vmid: "{{ proxmox_vmid }}"
|
|
|
|
{% endif %}
|
|
|
|
# state: present
|
|
|
|
headers:
|
|
|
|
# snapname: "{{ snap_name }}"
|
|
|
|
Title: >-
|
|
|
|
# description: "Pre-firmware-update: {{ firmware_current_version }} → {{ firmware_new_version }}"
|
|
|
|
{%- if m.skip_update | default(false) and b.skip_update | default(false) -%}
|
|
|
|
# timeout: 60
|
|
|
|
OPNsense cluster - no {{m.firmware_action }} available
|
|
|
|
# delegate_to: localhost
|
|
|
|
{%- else -%}
|
|
|
|
|
|
|
|
OPNsense cluster {{m.firmware_action }} completed
|
|
|
|
# - name: Enable CARP maintenance mode (triggers controlled failover to backup)
|
|
|
|
{%- endif -%}
|
|
|
|
# ansible.builtin.uri:
|
|
|
|
Priority: "{{ 'min' if (m.skip_update | default(false) and b.skip_update | default(false)) else 'default' }}"
|
|
|
|
# url: "https://{{ opnsense_host }}/api/carp/status/maintenancemode"
|
|
|
|
Tags: "{{ 'information_source' if (m.skip_update | default(false) and b.skip_update | default(false)) else 'white_check_mark' }}"
|
|
|
|
# method: POST
|
|
|
|
when: ntfy_url is defined
|
|
|
|
# user: "{{ opnsense_api_key }}"
|
|
|
|
|
|
|
|
# password: "{{ opnsense_api_secret }}"
|
|
|
|
|
|
|
|
# force_basic_auth: true
|
|
|
|
|
|
|
|
# validate_certs: false
|
|
|
|
|
|
|
|
# body_format: json
|
|
|
|
|
|
|
|
# body: {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# - name: Wait for CARP failover to settle
|
|
|
|
|
|
|
|
# ansible.builtin.pause:
|
|
|
|
|
|
|
|
# seconds: 10
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# # Do not proceed if the backup has not actually taken over.
|
|
|
|
|
|
|
|
# # A failed failover means no traffic coverage during master update.
|
|
|
|
|
|
|
|
# - name: Confirm backup node is now MASTER
|
|
|
|
|
|
|
|
# ansible.builtin.uri:
|
|
|
|
|
|
|
|
# url: "https://{{ hostvars[groups['opnsense_backup'][0]].ansible_host }}/api/carp/status/get"
|
|
|
|
|
|
|
|
# method: GET
|
|
|
|
|
|
|
|
# user: "{{ opnsense_api_key }}"
|
|
|
|
|
|
|
|
# password: "{{ opnsense_api_secret }}"
|
|
|
|
|
|
|
|
# force_basic_auth: true
|
|
|
|
|
|
|
|
# validate_certs: false
|
|
|
|
|
|
|
|
# register: _backup_carp
|
|
|
|
|
|
|
|
# delegate_to: localhost
|
|
|
|
|
|
|
|
# failed_when: _backup_carp.json.status | upper != 'MASTER'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# - name: Update and verify
|
|
|
|
|
|
|
|
# block:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# - 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
|
|
|
|
|
|
|
|
# body_format: json
|
|
|
|
|
|
|
|
# body: {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# - name: Wait for packages to install
|
|
|
|
|
|
|
|
# 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 != 'running'
|
|
|
|
|
|
|
|
# retries: 30
|
|
|
|
|
|
|
|
# delay: 15
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# - name: Reboot node
|
|
|
|
|
|
|
|
# ansible.builtin.uri:
|
|
|
|
|
|
|
|
# url: "https://{{ opnsense_host }}/api/core/firmware/reboot"
|
|
|
|
|
|
|
|
# method: POST
|
|
|
|
|
|
|
|
# user: "{{ opnsense_api_key }}"
|
|
|
|
|
|
|
|
# password: "{{ opnsense_api_secret }}"
|
|
|
|
|
|
|
|
# force_basic_auth: true
|
|
|
|
|
|
|
|
# validate_certs: false
|
|
|
|
|
|
|
|
# body_format: json
|
|
|
|
|
|
|
|
# body: {}
|
|
|
|
|
|
|
|
# ignore_errors: true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# - name: Wait for node to go offline
|
|
|
|
|
|
|
|
# ansible.builtin.wait_for:
|
|
|
|
|
|
|
|
# host: "{{ ansible_host }}"
|
|
|
|
|
|
|
|
# port: 443
|
|
|
|
|
|
|
|
# state: stopped
|
|
|
|
|
|
|
|
# timeout: 60
|
|
|
|
|
|
|
|
# delegate_to: localhost
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# - name: Wait for node to come back online
|
|
|
|
|
|
|
|
# ansible.builtin.wait_for:
|
|
|
|
|
|
|
|
# host: "{{ ansible_host }}"
|
|
|
|
|
|
|
|
# port: 443
|
|
|
|
|
|
|
|
# state: started
|
|
|
|
|
|
|
|
# timeout: 300
|
|
|
|
|
|
|
|
# delay: 30
|
|
|
|
|
|
|
|
# delegate_to: localhost
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# - 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: Health check — no remaining updates
|
|
|
|
|
|
|
|
# ansible.builtin.assert:
|
|
|
|
|
|
|
|
# that:
|
|
|
|
|
|
|
|
# - _post_status.json.status == 'none'
|
|
|
|
|
|
|
|
# fail_msg: >-
|
|
|
|
|
|
|
|
# Post-update firmware status is '{{ _post_status.json.status }}'.
|
|
|
|
|
|
|
|
# Expected 'none'. Triggering rollback.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# # After maintenance mode failover, master should rejoin as BACKUP
|
|
|
|
|
|
|
|
# - name: Health check — CARP role is BACKUP
|
|
|
|
|
|
|
|
# ansible.builtin.uri:
|
|
|
|
|
|
|
|
# url: "https://{{ opnsense_host }}/api/carp/status/get"
|
|
|
|
|
|
|
|
# method: GET
|
|
|
|
|
|
|
|
# user: "{{ opnsense_api_key }}"
|
|
|
|
|
|
|
|
# password: "{{ opnsense_api_secret }}"
|
|
|
|
|
|
|
|
# force_basic_auth: true
|
|
|
|
|
|
|
|
# validate_certs: false
|
|
|
|
|
|
|
|
# register: _carp_post
|
|
|
|
|
|
|
|
# failed_when: _carp_post.json.status | upper not in ['BACKUP', 'MAINTENANCE']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# - name: Record master update result
|
|
|
|
|
|
|
|
# ansible.builtin.set_fact:
|
|
|
|
|
|
|
|
# update_result: "success"
|
|
|
|
|
|
|
|
# fw_post_version: "{{ _post_status.json.product_version | default('unknown') }}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# rescue:
|
|
|
|
|
|
|
|
# # Snapshot was taken before maintenance mode, so the restored VM
|
|
|
|
|
|
|
|
# # will come back as a healthy MASTER on the previous version.
|
|
|
|
|
|
|
|
# # Backup (already updated) continues serving traffic during rollback.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# - name: "ROLLBACK | Stop VM"
|
|
|
|
|
|
|
|
# ansible.builtin.uri:
|
|
|
|
|
|
|
|
# url: "{{ _pve_base }}/status/stop"
|
|
|
|
|
|
|
|
# method: POST
|
|
|
|
|
|
|
|
# headers: "{{ _pve_auth }}"
|
|
|
|
|
|
|
|
# validate_certs: false
|
|
|
|
|
|
|
|
# delegate_to: localhost
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# - name: "ROLLBACK | Wait for VM to stop"
|
|
|
|
|
|
|
|
# ansible.builtin.uri:
|
|
|
|
|
|
|
|
# url: "{{ _pve_base }}/status/current"
|
|
|
|
|
|
|
|
# method: GET
|
|
|
|
|
|
|
|
# headers: "{{ _pve_auth }}"
|
|
|
|
|
|
|
|
# validate_certs: false
|
|
|
|
|
|
|
|
# register: _vm_state
|
|
|
|
|
|
|
|
# until: _vm_state.json.data.status == 'stopped'
|
|
|
|
|
|
|
|
# retries: 12
|
|
|
|
|
|
|
|
# delay: 5
|
|
|
|
|
|
|
|
# delegate_to: localhost
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# - name: "ROLLBACK | Restore snapshot {{ snap_name }}"
|
|
|
|
|
|
|
|
# ansible.builtin.uri:
|
|
|
|
|
|
|
|
# url: "{{ _pve_base }}/snapshot/{{ snap_name }}/rollback"
|
|
|
|
|
|
|
|
# method: POST
|
|
|
|
|
|
|
|
# headers: "{{ _pve_auth }}"
|
|
|
|
|
|
|
|
# validate_certs: false
|
|
|
|
|
|
|
|
# delegate_to: localhost
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# # Proxmox rollback tasks run async; wait for completion
|
|
|
|
|
|
|
|
# - name: "ROLLBACK | Wait for snapshot restore to complete"
|
|
|
|
|
|
|
|
# ansible.builtin.pause:
|
|
|
|
|
|
|
|
# seconds: 30
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# - name: "ROLLBACK | Start VM"
|
|
|
|
|
|
|
|
# ansible.builtin.uri:
|
|
|
|
|
|
|
|
# url: "{{ _pve_base }}/status/start"
|
|
|
|
|
|
|
|
# method: POST
|
|
|
|
|
|
|
|
# headers: "{{ _pve_auth }}"
|
|
|
|
|
|
|
|
# validate_certs: false
|
|
|
|
|
|
|
|
# delegate_to: localhost
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# - name: "ROLLBACK | Wait for VM to come back online"
|
|
|
|
|
|
|
|
# ansible.builtin.wait_for:
|
|
|
|
|
|
|
|
# host: "{{ ansible_host }}"
|
|
|
|
|
|
|
|
# port: 443
|
|
|
|
|
|
|
|
# state: started
|
|
|
|
|
|
|
|
# timeout: 300
|
|
|
|
|
|
|
|
# delay: 30
|
|
|
|
|
|
|
|
# delegate_to: localhost
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# - name: "ROLLBACK | Record result"
|
|
|
|
|
|
|
|
# ansible.builtin.set_fact:
|
|
|
|
|
|
|
|
# update_result: "rolled_back"
|
|
|
|
|
|
|
|
# fw_post_version: "{{ firmware_current_version }}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# always:
|
|
|
|
|
|
|
|
# # Best-effort: disable maintenance mode whether update succeeded or rolled back.
|
|
|
|
|
|
|
|
# # On rollback, the restored snapshot predates maintenance mode so this
|
|
|
|
|
|
|
|
# # is effectively a no-op, but run it anyway as a safety net.
|
|
|
|
|
|
|
|
# - 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
|
|
|
|
|
|
|
|
# body_format: json
|
|
|
|
|
|
|
|
# body: {}
|
|
|
|
|
|
|
|
# ignore_errors: true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ###############################################################
|
|
|
|
|
|
|
|
# # Phase 4 targets localhost — it runs even if Phase 3 failed,
|
|
|
|
|
|
|
|
# # because localhost was never marked as a failed host.
|
|
|
|
|
|
|
|
# - name: "Phase 4 | Send ntfy notification"
|
|
|
|
|
|
|
|
# hosts: localhost
|
|
|
|
|
|
|
|
# gather_facts: false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# tasks:
|
|
|
|
|
|
|
|
# - name: Build report
|
|
|
|
|
|
|
|
# ansible.builtin.set_fact:
|
|
|
|
|
|
|
|
# _report: |
|
|
|
|
|
|
|
|
# {% for host in groups['opnsense'] %}
|
|
|
|
|
|
|
|
# {% set h = hostvars[host] %}
|
|
|
|
|
|
|
|
# {{ host }}
|
|
|
|
|
|
|
|
# fw check : {{ h.firmware_status | default('unknown') }}
|
|
|
|
|
|
|
|
# result : {{ h.update_result | default('unknown') }}
|
|
|
|
|
|
|
|
# version : {{ h.fw_post_version | default(h.firmware_current_version | default('?')) }}
|
|
|
|
|
|
|
|
# {% endfor %}
|
|
|
|
|
|
|
|
# _any_issue: >-
|
|
|
|
|
|
|
|
# {{ groups['opnsense']
|
|
|
|
|
|
|
|
# | map('extract', hostvars, 'update_result')
|
|
|
|
|
|
|
|
# | select('in', ['rolled_back', 'pending'])
|
|
|
|
|
|
|
|
# | list
|
|
|
|
|
|
|
|
# | length > 0 }}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# - name: Send ntfy notification
|
|
|
|
|
|
|
|
# ansible.builtin.uri:
|
|
|
|
|
|
|
|
# url: "{{ ntfy_url }}/{{ ntfy_topic }}"
|
|
|
|
|
|
|
|
# method: POST
|
|
|
|
|
|
|
|
# headers:
|
|
|
|
|
|
|
|
# Authorization: "Bearer {{ ntfy_token }}"
|
|
|
|
|
|
|
|
# Title: "OPNsense HA Update Report"
|
|
|
|
|
|
|
|
# Priority: "{{ 'high' if _any_issue else 'default' }}"
|
|
|
|
|
|
|
|
# Tags: "{{ 'warning' if _any_issue else 'white_check_mark' }},shield"
|
|
|
|
|
|
|
|
# Content-Type: "text/plain"
|
|
|
|
|
|
|
|
# body: "{{ _report }}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# - name: Fail playbook if any node was rolled back or did not complete
|
|
|
|
|
|
|
|
# ansible.builtin.fail:
|
|
|
|
|
|
|
|
# msg: >-
|
|
|
|
|
|
|
|
# One or more nodes did not update successfully.
|
|
|
|
|
|
|
|
# Check ntfy report and Ansible output for details.
|
|
|
|
|
|
|
|
# when: _any_issue | bool
|