Compare commits

..
25 Commits
Author SHA1 Message Date
Vezpi 2918ea4578 add: requirements.yml for community.proxmox 2026-06-21 19:59:49 +00:00
Vezpi c959176ebd add: master update with Proxmox snapshoty 2026-06-21 19:59:27 +00:00
Vezpi 3d6511262e add: skip backuo 2026-06-19 20:18:54 +00:00
Vezpi a30bbddbf5 add: firmware update trigger 2026-06-19 12:51:08 +00:00
Vezpi 49a50e5af3 fix: use inventory_hostname 2026-06-19 12:40:30 +00:00
Vezpi 30f53ba20a fix: user ansible_inventory instead of host 2026-06-19 12:38:36 +00:00
Vezpi c1b9ea7e51 add: ntfy notification for pre-check 2026-06-19 12:35:42 +00:00
Vezpi 51e7886131 add: changed_when 2026-06-18 20:46:24 +00:00
Vezpi 72c13366e1 add: register when enable maintenance 2026-06-18 20:42:14 +00:00
Vezpi 70fbe3ba0a fix: variable 2026-06-18 20:36:22 +00:00
Vezpi 8d76623e3a add: start phase 2 2026-06-18 20:33:10 +00:00
Vezpi fbcbab5040 test: inventory var 2026-06-18 18:52:33 +00:00
Vezpi 0649dbdf6f add: get vip status 2026-06-18 18:47:34 +00:00
Vezpi a24575785b add: reboot info 2026-06-18 18:47:34 +00:00
Vezpi b168490adb fix: typo 2026-06-18 18:47:34 +00:00
Vezpi 0c5f5f866d fix: variables 2026-06-18 18:47:34 +00:00
Vezpi e242e7c945 fix: var name 2026-06-18 18:47:34 +00:00
Vezpi 6e85085a9a fix: host variable for API url 2026-06-18 18:47:34 +00:00
Vezpi d660f9f73e add: firmware check 2026-06-18 18:47:34 +00:00
Vezpi 8169f3023b Revert "test: unset validate certs"
This reverts commit 3c8375d6f4.
2026-06-18 18:47:34 +00:00
Vezpi 4e2dd232d3 test: unset validate certs 2026-06-18 18:47:34 +00:00
Vezpi b383ef6f42 fix: remove port 2026-06-18 18:47:34 +00:00
Vezpi 847e2a3c6b test: removed vars 2026-06-18 18:47:34 +00:00
Vezpi 5dee978c28 test: validate_certs to false 2026-06-18 18:47:34 +00:00
Vezpi 1dcf77a85f add: OPNsense HA cluster update playbook 2026-06-18 18:47:34 +00:00
7 changed files with 204 additions and 370 deletions
+197 -239
View File
@@ -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' or skip_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,7 +124,6 @@
- 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:
@@ -149,16 +141,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,16 +159,16 @@
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 delay: 15
- name: Wait for node to reboot after the {{ firmware_action }} - name: Wait for node to go offline
ansible.builtin.wait_for: ansible.builtin.wait_for:
host: "{{ ansible_host }}" host: "{{ ansible_host }}"
port: "{{ opnsense_https_port }}" port: "{{ opnsense_https_port }}"
state: stopped state: stopped
timeout: 3600 timeout: 60
when: needs_reboot when: needs_reboot
- name: Wait for node to come back online - name: Wait for node to come back online
@@ -184,11 +176,11 @@
host: "{{ ansible_host }}" host: "{{ ansible_host }}"
port: "{{ opnsense_https_port }}" port: "{{ opnsense_https_port }}"
state: started state: started
timeout: 5400 timeout: 300
delay: 30 delay: 30
when: needs_reboot when: needs_reboot
- name: Check firmware version - name: Wait for API to be fully responsive
ansible.builtin.uri: ansible.builtin.uri:
url: "https://{{ opnsense_host }}/api/core/firmware/status" url: "https://{{ opnsense_host }}/api/core/firmware/status"
method: GET method: GET
@@ -196,10 +188,10 @@
password: "{{ opnsense_api_secret }}" password: "{{ opnsense_api_secret }}"
force_basic_auth: true force_basic_auth: true
validate_certs: false validate_certs: false
register: _post_firmware_status register: _post_status
until: _post_firmware_status.json.product['product_' ~ firmware_target_kind] | default('unknown') == firmware_target_value until: _post_status.status == 200
retries: 240 retries: 20
delay: 15 delay: 10
- name: Check VIP status - name: Check VIP status
ansible.builtin.uri: ansible.builtin.uri:
@@ -213,14 +205,13 @@
- name: Disable CARP maintenance mode - name: Disable CARP maintenance mode
ansible.builtin.uri: ansible.builtin.uri:
url: "https://{{ opnsense_host }}/api/diagnostics/interface/carp_status/maintenance" url: "https://{{ opnsense_host }}/api/carp/status/maintenancemode"
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
when: _carp_post_udapte.json.carp.maintenancemode when: _carp_post_udapte.json.carp.maintenancemode
ignore_errors: true
rescue: rescue:
@@ -232,201 +223,168 @@
password: "{{ lookup('env', 'NTFY_PASSWORD') }}" password: "{{ lookup('env', 'NTFY_PASSWORD') }}"
force_basic_auth: true force_basic_auth: true
body: | body: |
Current version: {{ firmware_current_version }} TODO
{{ firmware_action | capitalize }} to {{ firmware_target_value }} failed on {{ inventory_hostname }} ({{ main_role }}). # Current version: {{ firmware_current_version }}
No snapshot have been taken before the {{ firmware_action }}. # {% 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: headers:
Title: "OPNsense {{ firmware_action }} failed on {{ inventory_hostname }}" Title: "OPNsense update failed on {{ inventory_hostname }}"
Priority: "high" Priority: "default"
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" Tags: "x"
run_once: true
when: ntfy_url is defined when: ntfy_url is defined
- ansible.builtin.fail: - ansible.builtin.fail:
msg: "Update aborted" msg: "Update aborted"
- name: "Phase 3 | Update master node (Proxmox, with snapshot)"
- name: "Phase 4 | Notification" hosts: opnsense_master
hosts: localhost
gather_facts: false gather_facts: false
vars:
proxmox_snap_name: "preupdate-{{ now().strftime('%Y%m%d-%H%M') }}"
tasks: tasks:
- name: Send Ntfy notification - block:
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: - name: Take Proxmox VM snapshot
url: "{{ ntfy_url }}/{{ ntfy_topic }}" community.general.proxmox_snap:
method: POST vmid: "{{ proxmox_vmid }}"
user: "{{ ntfy_user }}" state: present
password: "{{ lookup('env', 'NTFY_PASSWORD') }}" snapname: "{{ proxmox_snap_name }}"
force_basic_auth: true description: "Pre-firmware-update: {{ firmware_current_version }} → {{ firmware_new_version }}"
body: |
{% if same_operation %} - name: Enable CARP maintenance mode
{% if m.skip_update | default(false) %} ansible.builtin.uri:
Both nodes already on {{ m.firmware_current_version }}, no action taken. url: "https://{{ opnsense_host }}/api/diagnostics/interface/carp_status/maintenance"
{% else %} method: POST
OPNsense cluster: {{ m.firmware_current_version }} → {{ m.firmware_target_value }} ({{ m.firmware_status }}) user: "{{ opnsense_api_key }}"
{% endif %} password: "{{ opnsense_api_secret }}"
{% else %} force_basic_auth: true
{% if m.skip_update | default(false) %} validate_certs: false
MASTER ({{ master }}): already on {{ m.firmware_current_version }}, no action taken. register: _enable_maintenance
{% else %} changed_when: _enable_maintenance.status == "ok"
MASTER ({{ master }}): {{ m.firmware_current_version }} → {{ m.firmware_target_value }} ({{ m.firmware_status }})
{% endif %} - name: Trigger firmware update
{% if b.skip_update | default(false) %} ansible.builtin.uri:
BACKUP ({{ backup }}): already on {{ b.firmware_current_version }}, no action taken. url: "https://{{ opnsense_host }}/api/core/firmware/update"
{% else %} method: POST
BACKUP ({{ backup }}): {{ b.firmware_current_version }} → {{ b.firmware_target_value }} ({{ b.firmware_status }}) user: "{{ opnsense_api_key }}"
{% endif %} password: "{{ opnsense_api_secret }}"
{% endif %} force_basic_auth: true
headers: validate_certs: false
Title: >-
{%- if m.skip_update | default(false) and b.skip_update | default(false) -%} - name: Wait for packages to update
OPNsense cluster - no {{m.firmware_action }} available ansible.builtin.uri:
{%- else -%} url: "https://{{ opnsense_host }}/api/core/firmware/running"
OPNsense cluster {{m.firmware_action }} completed method: GET
{%- endif -%} user: "{{ opnsense_api_key }}"
Priority: "{{ 'min' if (m.skip_update | default(false) and b.skip_update | default(false)) else 'default' }}" password: "{{ opnsense_api_secret }}"
Tags: "{{ 'information_source' if (m.skip_update | default(false) and b.skip_update | default(false)) else 'white_check_mark' }}" force_basic_auth: true
when: ntfy_url is defined 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"
-120
View File
@@ -1,120 +0,0 @@
---
- name: Create or update a cloud-init Ubuntu template
hosts: nodes
become: true
vars:
template_vmid: 900
template_os: ubuntu
os_release: noble
template_name: "{{template_os}}-{{ os_release }}-cloud"
image_filename: "{{ os_release }}-server-cloudimg-amd64.img"
image_url: "https://cloud-images.ubuntu.com/{{ os_release }}/current/{{ image_filename }}"
image_dest: "/var/lib/vz/template/iso/{{ image_filename }}"
iso_storage: local
vm_storage: ceph-workload
template_memory: 2048
template_cores: 1
tasks:
- name: Check whether the template VMID already exists
ansible.builtin.command: qm status {{ template_vmid }}
register: template_status
changed_when: false
failed_when: false
- name: Check if the cloud image file exists
ansible.builtin.stat:
path: "{{ image_dest }}"
register: image_exists
changed_when: false
failed_when: false
- name: Set candidate information
ansible.builtin.set_fact:
has_image_file: "{{ image_exists.stat.exists }}"
root_free_space: "{{ (ansible_facts['mounts'] | selectattr('mount', 'equalto', '/') | first).size_available }}"
- name: Select target host
ansible.builtin.set_fact:
target_host: >-
{{ (ansible_play_hosts
| map('extract', hostvars)
| selectattr('has_image_file', 'equalto', true)
| sort(attribute='root_free_space', reverse=true)
| map(attribute='inventory_hostname')
| first)
| default(ansible_play_hosts
| map('extract', hostvars)
| sort(attribute='root_free_space', reverse=true)
| map(attribute='inventory_hostname')
| first,
true) }}
run_once: true
- name: Download the current Ubuntu cloud image
ansible.builtin.get_url:
url: "{{ image_url }}"
dest: "{{ image_dest }}"
force: false
mode: "0644"
register: image_download
when: inventory_hostname == target_host
- name: Destroy existing template if present
ansible.builtin.command: qm destroy {{ template_vmid }} --purge
when:
- template_status.rc == 0
- image_download.changed
- name: Rebuild the template
when:
- inventory_hostname == target_host
- image_download.changed or
template_status.rc != 0
block:
- name: Create the base VM shell
ansible.builtin.command: >
qm create {{ template_vmid }}
--memory {{ template_memory }}
--core {{ template_cores }}
--net0 virtio,bridge=vmbr0
--scsihw virtio-scsi-pci
--bios ovmf
--machine q35
--efidisk0 {{ vm_storage }}:0,pre-enrolled-keys=0
--name {{ template_name }}
- name: Import the cloud image as the primary disk
ansible.builtin.command: qm set {{ template_vmid }} --scsi0 {{ vm_storage }}:0,import-from={{ image_dest }}
- name: Attach a cloud-init drive
ansible.builtin.command: qm set {{ template_vmid }} --scsi1 {{ vm_storage }}:cloudinit
- name: Set boot order to the primary disk
ansible.builtin.command: qm set {{ template_vmid }} --boot order=scsi0
- name: Add serial console for cloud-init consoles
ansible.builtin.command: qm set {{ template_vmid }} --serial0 socket --vga serial0
- name: Convert the VM to a template
ansible.builtin.command: qm template {{ template_vmid }}
- name: Send notification
ansible.builtin.uri:
url: "{{ ntfy_url }}/{{ ntfy_topic }}"
method: POST
user: "{{ ntfy_user }}"
password: "{{ lookup('env', 'NTFY_PASSWORD') }}"
force_basic_auth: true
body: |
The {{ template_os | capitalize }} {{ os_release }} cloud-init template has been successfully updated on {{ inventory_hostname }}.
headers:
Title: "Cloud-init template updated"
Priority: "min"
Tags: "white_check_mark"
when: ntfy_url is defined
-4
View File
@@ -94,10 +94,6 @@
retries: 60 retries: 60
delay: 15 delay: 15
- name: Pause 15s
ansible.builtin.pause:
seconds: 15
- name: Update packages - name: Update packages
ansible.builtin.apt: ansible.builtin.apt:
upgrade: full upgrade: full
+3 -3
View File
@@ -84,7 +84,7 @@ if ($type === "MASTER") {
write_config("disable interface '$ifkey' due CARP event '$type'", false); write_config("disable interface '$ifkey' due CARP event '$type'", false);
interface_configure(false, $ifkey, false, false); interface_configure(false, $ifkey, false, false);
// Remove WAN default gateway // Remove WAN default gateway
mwexecf("/sbin/route delete default"); mwexec("/sbin/route delete default");
foreach ($config['OPNsense']['Gateways']['gateway_item'] as $gw) { foreach ($config['OPNsense']['Gateways']['gateway_item'] as $gw) {
if ($gw['name'] === $gw_name) { if ($gw['name'] === $gw_name) {
$gw_ip = $gw['gateway']; $gw_ip = $gw['gateway'];
@@ -92,9 +92,9 @@ if ($type === "MASTER") {
} }
} }
// Shutdown WAN interface // Shutdown WAN interface
mwexecf("/sbin/ifconfig {$real_if} down") mwexec("/sbin/ifconfig {$real_if} down")
// Add fallback default gateway // Add fallback default gateway
mwexecf("/sbin/route add default {$gw_ip}"); mwexec("/sbin/route add default {$gw_ip}");
// Create lock file // Create lock file
file_put_contents($lock_file, time()); file_put_contents($lock_file, time());
sleep($lock_default_age); sleep($lock_default_age);
+1 -1
View File
@@ -12,7 +12,7 @@ variable "node_datastore" {
variable "vm_template" { variable "vm_template" {
description = "Template of the VM" description = "Template of the VM"
type = string type = string
default = "ubuntu-noble-cloud" default = "ubuntu-cloud"
} }
variable "vm_name" { variable "vm_name" {
+1 -1
View File
@@ -6,7 +6,7 @@ resource "proxmox_vm_qemu" "cloudinit-test" {
target_node = "zenith" target_node = "zenith"
clone = "ubuntu-noble-cloud" clone = "ubuntu-cloud"
agent = 0 agent = 0
+1 -1
View File
@@ -23,7 +23,7 @@ variable "node_datastore" {
variable "vm_template" { variable "vm_template" {
description = "Template of the VM" description = "Template of the VM"
type = string type = string
default = "ubuntu-noble-cloud" default = "ubuntu-cloud"
} }
variable "vm_name" { variable "vm_name" {