Compare commits
3 Commits
lab
...
29a0d32870
| Author | SHA1 | Date | |
|---|---|---|---|
| 29a0d32870 | |||
| 22f6403417 | |||
| a7491df751 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,6 +1,4 @@
|
|||||||
# Terraform
|
# Terraform
|
||||||
**/.terraform*
|
**/.terraform*
|
||||||
*.tfstate*
|
*.tfstate*
|
||||||
*credentials.auto.tfvars
|
*credentials.auto.tfvars
|
||||||
# Bruno
|
|
||||||
.bruno
|
|
||||||
21
README.md
21
README.md
@@ -1,20 +1,3 @@
|
|||||||
# 🧪 Homelab
|
# Homelab
|
||||||
|
|
||||||
> ⚠️ **Work in Progress** – This repository is actively evolving as I automate and expand my homelab.
|
Hello world !
|
||||||
|
|
||||||
Welcome to my homelab repository! This is where I manage and document the infrastructure powering my personal lab environment using modern DevOps tools and best practices.
|
|
||||||
|
|
||||||
## 🚀 Goals
|
|
||||||
|
|
||||||
- Automate VM and infrastructure deployment with **Terraform**
|
|
||||||
- Configure systems and services using **Ansible**
|
|
||||||
- Deploy and manage Kubernetes with **Flux CD** using a **GitOps** approach
|
|
||||||
- Keep everything **declarative**, **reproducible**, and **version-controlled**
|
|
||||||
|
|
||||||
## 📌 Notes
|
|
||||||
|
|
||||||
This repository is intended for **educational and experimental purposes**. Feel free to explore, fork, and adapt ideas for your own homelab setup.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Stay tuned — more coming soon! 🚧
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Demo Playbook - Install Nginx and Serve Hostname Page
|
|
||||||
hosts: all
|
|
||||||
become: true
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
- name: Ensure apt cache is updated
|
|
||||||
ansible.builtin.apt:
|
|
||||||
update_cache: true
|
|
||||||
cache_valid_time: 3600
|
|
||||||
|
|
||||||
- name: Install nginx
|
|
||||||
ansible.builtin.apt:
|
|
||||||
name: nginx
|
|
||||||
state: present
|
|
||||||
|
|
||||||
- name: Create index.html with hostname
|
|
||||||
ansible.builtin.copy:
|
|
||||||
dest: /var/www/html/index.html
|
|
||||||
content: |
|
|
||||||
<html>
|
|
||||||
<head><title>Demo</title></head>
|
|
||||||
<body>
|
|
||||||
<h1>Hostname: {{ inventory_hostname }}</h1>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
owner: www-data
|
|
||||||
group: www-data
|
|
||||||
mode: "0644"
|
|
||||||
|
|
||||||
- name: Ensure nginx is running
|
|
||||||
ansible.builtin.service:
|
|
||||||
name: nginx
|
|
||||||
state: started
|
|
||||||
enabled: true
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
---
|
|
||||||
all:
|
|
||||||
children:
|
|
||||||
opnsense:
|
|
||||||
vars:
|
|
||||||
ansible_connection: local
|
|
||||||
children:
|
|
||||||
opnsense_backup:
|
|
||||||
hosts:
|
|
||||||
cerbere-head2:
|
|
||||||
ansible_host: 192.168.88.3
|
|
||||||
main_role: BACKUP
|
|
||||||
hypervisor: TrueNAS
|
|
||||||
opnsense_master:
|
|
||||||
hosts:
|
|
||||||
cerbere-head1:
|
|
||||||
ansible_host: 192.168.88.2
|
|
||||||
main_role: MASTER
|
|
||||||
hypervisor: Proxmox
|
|
||||||
proxmox_vmid: 122
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
---
|
|
||||||
collections:
|
|
||||||
- name: community.proxmox
|
|
||||||
version: "2.0.0"
|
|
||||||
@@ -1,432 +0,0 @@
|
|||||||
---
|
|
||||||
# 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
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Create Terraform local user for Proxmox
|
|
||||||
hosts: nodes
|
|
||||||
become: true
|
|
||||||
tasks:
|
|
||||||
|
|
||||||
- name: Create terraform user
|
|
||||||
ansible.builtin.user:
|
|
||||||
name: "{{ terraform_user }}"
|
|
||||||
password: "{{ terraform_password | password_hash('sha512') }}"
|
|
||||||
shell: /bin/bash
|
|
||||||
|
|
||||||
- name: Create sudoers file for terraform user
|
|
||||||
ansible.builtin.copy:
|
|
||||||
dest: /etc/sudoers.d/{{ terraform_user }}
|
|
||||||
mode: '0440'
|
|
||||||
content: |
|
|
||||||
{{ terraform_user }} ALL=(root) NOPASSWD: /sbin/pvesm
|
|
||||||
{{ terraform_user }} ALL=(root) NOPASSWD: /sbin/qm
|
|
||||||
{{ terraform_user }} ALL=(root) NOPASSWD: /usr/bin/tee /var/lib/vz/*
|
|
||||||
@@ -1,204 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Check Proxmox VE cluster health
|
|
||||||
hosts: nodes
|
|
||||||
any_errors_fatal: true
|
|
||||||
become: true
|
|
||||||
tasks:
|
|
||||||
|
|
||||||
- delegate_to: "{{ groups['nodes'][0] }}"
|
|
||||||
run_once: true
|
|
||||||
block:
|
|
||||||
|
|
||||||
- name: Verify cluster quorum
|
|
||||||
ansible.builtin.command: pvecm status
|
|
||||||
register: quorum_status
|
|
||||||
changed_when: false
|
|
||||||
failed_when: quorum_status.stdout is not search('Quorate:\\s*Yes')
|
|
||||||
|
|
||||||
- name: Verify Ceph health
|
|
||||||
ansible.builtin.command: ceph health
|
|
||||||
register: ceph_health
|
|
||||||
changed_when: false
|
|
||||||
failed_when: "'HEALTH_OK' not in ceph_health.stdout"
|
|
||||||
|
|
||||||
rescue:
|
|
||||||
|
|
||||||
- name: Send no ready notification
|
|
||||||
ansible.builtin.uri:
|
|
||||||
url: "{{ ntfy_url }}/{{ ntfy_topic }}"
|
|
||||||
method: POST
|
|
||||||
user: "{{ ntfy_user }}"
|
|
||||||
password: "{{ lookup('env', 'NTFY_PASSWORD') }}"
|
|
||||||
force_basic_auth: true
|
|
||||||
body: No updates have been rolled out
|
|
||||||
headers:
|
|
||||||
Title: "Proxmox VE Not Ready for Updates"
|
|
||||||
Priority: "default"
|
|
||||||
Tags: "x"
|
|
||||||
delegate_to: localhost
|
|
||||||
become: false
|
|
||||||
run_once: true
|
|
||||||
when: ntfy_url is defined
|
|
||||||
|
|
||||||
- ansible.builtin.fail:
|
|
||||||
msg: "Update aborted"
|
|
||||||
|
|
||||||
|
|
||||||
- name: Rolling update of Proxmox VE cluster
|
|
||||||
hosts: nodes
|
|
||||||
serial: 1
|
|
||||||
any_errors_fatal: true
|
|
||||||
become: true
|
|
||||||
tasks:
|
|
||||||
|
|
||||||
- block:
|
|
||||||
|
|
||||||
- name: Refresh repositories
|
|
||||||
ansible.builtin.apt:
|
|
||||||
update_cache: true
|
|
||||||
|
|
||||||
- name: Check if updates are available
|
|
||||||
ansible.builtin.apt:
|
|
||||||
upgrade: dist
|
|
||||||
check_mode: true
|
|
||||||
register: apt_check
|
|
||||||
|
|
||||||
- name: Proceed if updates are available
|
|
||||||
when: apt_check.changed
|
|
||||||
block:
|
|
||||||
|
|
||||||
- name: Get version before upgrade
|
|
||||||
ansible.builtin.shell: pveversion | awk -F'/' '{print $2}'
|
|
||||||
register: pve_old_version
|
|
||||||
changed_when: false
|
|
||||||
|
|
||||||
- name: Enable maintenance mode
|
|
||||||
ansible.builtin.command: >
|
|
||||||
ha-manager crm-command node-maintenance enable {{ inventory_hostname_short }}
|
|
||||||
|
|
||||||
- name: Wait for LXCs to leave node
|
|
||||||
ansible.builtin.shell: |
|
|
||||||
pct list | awk 'NR>1 && $2=="running" {count++} END {print count+0}'
|
|
||||||
register: lxc_count
|
|
||||||
changed_when: false
|
|
||||||
until: lxc_count.stdout | int == 0
|
|
||||||
retries: 60
|
|
||||||
delay: 15
|
|
||||||
|
|
||||||
- name: Wait for VMs to leave node
|
|
||||||
ansible.builtin.shell: |
|
|
||||||
qm list | awk 'NR>1 && $3=="running" {count++} END {print count+0}'
|
|
||||||
register: vm_count
|
|
||||||
changed_when: false
|
|
||||||
until: vm_count.stdout | int == 0
|
|
||||||
retries: 60
|
|
||||||
delay: 15
|
|
||||||
|
|
||||||
- name: Pause 15s
|
|
||||||
ansible.builtin.pause:
|
|
||||||
seconds: 15
|
|
||||||
|
|
||||||
- name: Update packages
|
|
||||||
ansible.builtin.apt:
|
|
||||||
upgrade: full
|
|
||||||
autoremove: true
|
|
||||||
autoclean: true
|
|
||||||
|
|
||||||
- name: Disable Ceph rebalancing
|
|
||||||
ansible.builtin.command: ceph osd set noout
|
|
||||||
|
|
||||||
- name: Reboot node
|
|
||||||
ansible.builtin.reboot:
|
|
||||||
reboot_timeout: 900
|
|
||||||
post_reboot_delay: 30
|
|
||||||
|
|
||||||
- name: Enable Ceph rebalancing
|
|
||||||
ansible.builtin.command: ceph osd unset noout
|
|
||||||
|
|
||||||
- name: Disable maintenance mode
|
|
||||||
ansible.builtin.command: >
|
|
||||||
ha-manager crm-command node-maintenance disable {{ inventory_hostname_short }}
|
|
||||||
|
|
||||||
- name: Get version after upgrade
|
|
||||||
ansible.builtin.shell: pveversion | awk -F'/' '{print $2}'
|
|
||||||
register: pve_new_version
|
|
||||||
changed_when: false
|
|
||||||
|
|
||||||
- name: Save update report
|
|
||||||
ansible.builtin.set_fact:
|
|
||||||
update_report:
|
|
||||||
old: "{{ pve_old_version.stdout }}"
|
|
||||||
new: "{{ pve_new_version.stdout }}"
|
|
||||||
|
|
||||||
- name: Wait for Ceph to be healthy
|
|
||||||
ansible.builtin.command: ceph health
|
|
||||||
register: ceph_status
|
|
||||||
changed_when: false
|
|
||||||
until: "'HEALTH_OK' in ceph_status.stdout"
|
|
||||||
retries: 60
|
|
||||||
delay: 15
|
|
||||||
delegate_to: "{{ groups['nodes'][0] }}"
|
|
||||||
|
|
||||||
rescue:
|
|
||||||
|
|
||||||
- name: Send failure notification
|
|
||||||
ansible.builtin.uri:
|
|
||||||
url: "{{ ntfy_url }}/{{ ntfy_topic }}"
|
|
||||||
method: POST
|
|
||||||
user: "{{ ntfy_user }}"
|
|
||||||
password: "{{ lookup('env', 'NTFY_PASSWORD') }}"
|
|
||||||
force_basic_auth: true
|
|
||||||
body: Update failed on {{ inventory_hostname_short }}
|
|
||||||
headers:
|
|
||||||
Title: "Proxmox VE Update Failed"
|
|
||||||
Priority: "high"
|
|
||||||
Tags: "x"
|
|
||||||
delegate_to: localhost
|
|
||||||
become: false
|
|
||||||
run_once: true
|
|
||||||
when: ntfy_url is defined
|
|
||||||
|
|
||||||
- ansible.builtin.fail:
|
|
||||||
msg: "Update aborted"
|
|
||||||
|
|
||||||
|
|
||||||
- name: Send notification
|
|
||||||
hosts: localhost
|
|
||||||
tasks:
|
|
||||||
|
|
||||||
- name: Determine if updates occurred
|
|
||||||
ansible.builtin.set_fact:
|
|
||||||
updates_performed: "{{ groups['nodes'] | map('extract', hostvars) | selectattr('update_report', 'defined') | list | length > 0 }}"
|
|
||||||
|
|
||||||
- name: Send success notification
|
|
||||||
ansible.builtin.uri:
|
|
||||||
url: "{{ ntfy_url }}/{{ ntfy_topic }}"
|
|
||||||
method: POST
|
|
||||||
user: "{{ ntfy_user }}"
|
|
||||||
password: "{{ lookup('env', 'NTFY_PASSWORD') }}"
|
|
||||||
force_basic_auth: true
|
|
||||||
body: |
|
|
||||||
{% set updated_nodes = [] %}
|
|
||||||
{% for node in groups['nodes'] %}
|
|
||||||
{% if hostvars[node].update_report is defined %}
|
|
||||||
{% set _ = updated_nodes.append(node) %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{% if not updates_performed %}
|
|
||||||
No updates available on the cluster.
|
|
||||||
{% else %}
|
|
||||||
The following nodes were updated:
|
|
||||||
{% for node in updated_nodes %}
|
|
||||||
{% if hostvars[node].update_report.old == hostvars[node].update_report.new %}
|
|
||||||
- {{ hostvars[node].inventory_hostname_short }}: version {{ hostvars[node].update_report.old }} (unchanged)
|
|
||||||
{% else %}
|
|
||||||
- {{ hostvars[node].inventory_hostname_short }}: version {{ hostvars[node].update_report.old }} → {{ hostvars[node].update_report.new }}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
headers:
|
|
||||||
Title: "Proxmox VE Update Report"
|
|
||||||
Priority: "{{ 'min' if not updates_performed else 'default' }}"
|
|
||||||
Tags: "white_check_mark"
|
|
||||||
when: ntfy_url is defined
|
|
||||||
|
|
||||||
105
opnsense/10-wan
105
opnsense/10-wan
@@ -1,105 +0,0 @@
|
|||||||
#!/usr/local/bin/php
|
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* Author 2025 Etienne Girault <etienne.girault@gmail.com>
|
|
||||||
* OPNsense CARP event script
|
|
||||||
* - Enables/disables the WAN interface only when needed
|
|
||||||
* - Avoids reapplying config when CARP triggers multiple times
|
|
||||||
*/
|
|
||||||
|
|
||||||
require_once("config.inc");
|
|
||||||
require_once("interfaces.inc");
|
|
||||||
require_once("util.inc");
|
|
||||||
require_once("system.inc");
|
|
||||||
|
|
||||||
// Read CARP event arguments
|
|
||||||
$subsystem = !empty($argv[1]) ? $argv[1] : '';
|
|
||||||
$type = !empty($argv[2]) ? $argv[2] : '';
|
|
||||||
|
|
||||||
// Accept only MASTER/BACKUP events
|
|
||||||
if (!in_array($type, ['MASTER', 'BACKUP'])) {
|
|
||||||
// Ignore CARP INIT, DEMOTED, etc.
|
|
||||||
exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate subsystem name format, expected pattern: <ifname>@<vhid>
|
|
||||||
if (!preg_match('/^[a-z0-9_]+@\S+$/i', $subsystem)) {
|
|
||||||
log_error("Malformed subsystem argument: '{$subsystem}'.");
|
|
||||||
exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only react to the primary VHID
|
|
||||||
list($vhid, $iface) = explode('@', $subsystem);
|
|
||||||
$primary_vhid = '1'; //
|
|
||||||
|
|
||||||
if ($vhid !== $primary_vhid) {
|
|
||||||
exit(0); // ignore events from other VHIDs
|
|
||||||
}
|
|
||||||
|
|
||||||
// Interface key to manage
|
|
||||||
$ifkey = 'wan';
|
|
||||||
$real_if = get_real_interface('wan');
|
|
||||||
// Fallback gateway name
|
|
||||||
$gw_name = 'LAN_GW';
|
|
||||||
// Determine whether WAN interface is currently enabled
|
|
||||||
$ifkey_enabled = !empty($config['interfaces'][$ifkey]['enable']) ? true : false;
|
|
||||||
// Lock file to prevent interface flapping
|
|
||||||
$lock_file = '/tmp/carp_wan_disable_lock';
|
|
||||||
$lock_default_age = 5;
|
|
||||||
$lock_max_age = 10;
|
|
||||||
|
|
||||||
// MASTER event
|
|
||||||
if ($type === "MASTER") {
|
|
||||||
// Enable WAN only if it's currently disabled
|
|
||||||
if (!$ifkey_enabled) {
|
|
||||||
// Check if lock file is present
|
|
||||||
if (file_exists($lock_file)) {
|
|
||||||
$lock_age = time() - (int)file_get_contents($lock_file);
|
|
||||||
if ($lock_age < $lock_max_age) {
|
|
||||||
log_msg("CARP event: WAN disable lock present ({$lock_age}s old), waiting...");
|
|
||||||
$elapsed = 0;
|
|
||||||
while (file_exists($lock_file) && $elapsed < 5000) {
|
|
||||||
usleep(500000);
|
|
||||||
$elapsed += 500;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log_msg("CARP event: removing stale WAN disable lock.");
|
|
||||||
@unlink($lock_file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log_msg("CARP event: switching to '$type', enabling interface '$ifkey'.", LOG_WARNING);
|
|
||||||
$config['interfaces'][$ifkey]['enable'] = '1';
|
|
||||||
write_config("enable interface '$ifkey' due CARP event '$type'", false);
|
|
||||||
interface_configure(false, $ifkey, false, false);
|
|
||||||
} else {
|
|
||||||
log_msg("CARP event: already 'MASTER' for interface '$ifkey', nothing to do.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// BACKUP event
|
|
||||||
} else {
|
|
||||||
// Disable WAN only if it's currently enabled
|
|
||||||
if ($ifkey_enabled) {
|
|
||||||
log_msg("CARP event: switching to '$type', disabling interface '$ifkey'.", LOG_WARNING);
|
|
||||||
unset($config['interfaces'][$ifkey]['enable']);
|
|
||||||
write_config("disable interface '$ifkey' due CARP event '$type'", false);
|
|
||||||
interface_configure(false, $ifkey, false, false);
|
|
||||||
// Remove WAN default gateway
|
|
||||||
mwexecf("/sbin/route delete default");
|
|
||||||
foreach ($config['OPNsense']['Gateways']['gateway_item'] as $gw) {
|
|
||||||
if ($gw['name'] === $gw_name) {
|
|
||||||
$gw_ip = $gw['gateway'];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Shutdown WAN interface
|
|
||||||
mwexecf("/sbin/ifconfig {$real_if} down")
|
|
||||||
// Add fallback default gateway
|
|
||||||
mwexecf("/sbin/route add default {$gw_ip}");
|
|
||||||
// Create lock file
|
|
||||||
file_put_contents($lock_file, time());
|
|
||||||
sleep($lock_default_age);
|
|
||||||
@unlink($lock_file);
|
|
||||||
} else {
|
|
||||||
log_msg("CARP event: already '$type' for interface '$ifkey', nothing to do.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,107 +1,93 @@
|
|||||||
# Retrieve VM templates available in Proxmox that match the specified name
|
|
||||||
data "proxmox_virtual_environment_vms" "template" {
|
data "proxmox_virtual_environment_vms" "template" {
|
||||||
filter {
|
filter {
|
||||||
name = "name"
|
name = "name"
|
||||||
values = ["${var.vm_template}"] # The name of the template to clone from
|
values = ["${var.vm_template}"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Create a cloud-init configuration file as a Proxmox snippet
|
|
||||||
resource "proxmox_virtual_environment_file" "cloud_config" {
|
resource "proxmox_virtual_environment_file" "cloud_config" {
|
||||||
content_type = "snippets" # Cloud-init files are stored as snippets in Proxmox
|
content_type = "snippets"
|
||||||
datastore_id = "local" # Local datastore used to store the snippet
|
datastore_id = "local"
|
||||||
node_name = var.node_name # The Proxmox node where the file will be uploaded
|
node_name = var.node_name
|
||||||
|
|
||||||
source_raw {
|
source_raw {
|
||||||
file_name = "${var.vm_name}.cloud-config.yaml" # The name of the snippet file
|
file_name = "${var.vm_name}.cloud-config.yaml"
|
||||||
data = <<-EOF
|
data = <<-EOF
|
||||||
#cloud-config
|
#cloud-config
|
||||||
hostname: ${var.vm_name}
|
hostname: ${var.vm_name}
|
||||||
package_update: true
|
package_update: true
|
||||||
package_upgrade: true
|
package_upgrade: true
|
||||||
packages:
|
packages:
|
||||||
- qemu-guest-agent # Ensures the guest agent is installed
|
- qemu-guest-agent
|
||||||
users:
|
users:
|
||||||
- default
|
- default
|
||||||
- name: ${var.vm_user}
|
- name: ${var.vm_user}
|
||||||
groups: sudo
|
groups: sudo
|
||||||
shell: /bin/bash
|
shell: /bin/bash
|
||||||
ssh-authorized-keys: ${jsonencode(var.vm_user_sshkeys)} # Inject user's SSH key
|
ssh-authorized-keys:
|
||||||
|
- "${var.vm_user_sshkey}"
|
||||||
sudo: ALL=(ALL) NOPASSWD:ALL
|
sudo: ALL=(ALL) NOPASSWD:ALL
|
||||||
runcmd:
|
runcmd:
|
||||||
- systemctl enable qemu-guest-agent
|
- systemctl enable qemu-guest-agent
|
||||||
- reboot # Reboot the VM after provisioning
|
- reboot
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Define and provision a new VM by cloning the template and applying initialization
|
|
||||||
resource "proxmox_virtual_environment_vm" "vm" {
|
resource "proxmox_virtual_environment_vm" "vm" {
|
||||||
name = var.vm_name # VM name
|
name = var.vm_name
|
||||||
node_name = var.node_name # Proxmox node to deploy the VM
|
node_name = var.node_name
|
||||||
tags = var.vm_tags # Optional VM tags for categorization
|
tags = var.vm_tags
|
||||||
|
|
||||||
agent {
|
agent {
|
||||||
enabled = true # Enable the QEMU guest agent
|
enabled = true
|
||||||
}
|
}
|
||||||
|
stop_on_destroy = true
|
||||||
stop_on_destroy = true # Ensure VM is stopped gracefully when destroyed
|
|
||||||
|
|
||||||
clone {
|
clone {
|
||||||
vm_id = data.proxmox_virtual_environment_vms.template.vms[0].vm_id # ID of the source template
|
vm_id = data.proxmox_virtual_environment_vms.template.vms[0].vm_id
|
||||||
node_name = data.proxmox_virtual_environment_vms.template.vms[0].node_name # Node of the source template
|
node_name = data.proxmox_virtual_environment_vms.template.vms[0].node_name
|
||||||
}
|
}
|
||||||
|
bios = var.vm_bios
|
||||||
bios = var.vm_bios # BIOS type (e.g., seabios or ovmf)
|
machine = var.vm_machine
|
||||||
machine = var.vm_machine # Machine type (e.g., q35)
|
|
||||||
|
|
||||||
cpu {
|
cpu {
|
||||||
cores = var.vm_cpu # Number of CPU cores
|
cores = var.vm_cpu
|
||||||
type = "host" # Use host CPU type for best compatibility/performance
|
type = "host"
|
||||||
}
|
}
|
||||||
|
|
||||||
memory {
|
memory {
|
||||||
dedicated = var.vm_ram # RAM in MB
|
dedicated = var.vm_ram
|
||||||
}
|
}
|
||||||
|
|
||||||
disk {
|
disk {
|
||||||
datastore_id = var.node_datastore # Datastore to hold the disk
|
datastore_id = var.node_datastore
|
||||||
interface = "scsi0" # Primary disk interface
|
interface = "scsi0"
|
||||||
size = var.vm_disk_size # Disk size in GB
|
size = 4
|
||||||
}
|
}
|
||||||
|
|
||||||
initialization {
|
initialization {
|
||||||
user_data_file_id = proxmox_virtual_environment_file.cloud_config.id # Link the cloud-init file
|
user_data_file_id = proxmox_virtual_environment_file.cloud_config.id
|
||||||
datastore_id = var.node_datastore
|
datastore_id = var.node_datastore
|
||||||
interface = "scsi1" # Separate interface for cloud-init
|
interface = "scsi1"
|
||||||
ip_config {
|
ip_config {
|
||||||
ipv4 {
|
ipv4 {
|
||||||
address = "dhcp" # Get IP via DHCP
|
address = "dhcp"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
network_device {
|
network_device {
|
||||||
bridge = "vlan${var.vm_vlan}" # VNet used with VLAN ID
|
bridge = "vmbr0"
|
||||||
|
vlan_id = var.vm_vlan
|
||||||
}
|
}
|
||||||
|
|
||||||
operating_system {
|
operating_system {
|
||||||
type = "l26" # Linux 2.6+ kernel
|
type = "l26"
|
||||||
}
|
}
|
||||||
|
|
||||||
vga {
|
vga {
|
||||||
type = "std" # Standard VGA type
|
type = "std"
|
||||||
}
|
}
|
||||||
|
|
||||||
lifecycle {
|
lifecycle {
|
||||||
ignore_changes = [ # Ignore initialization section after first depoloyment for idempotency
|
ignore_changes = [
|
||||||
initialization
|
initialization
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Output the assigned IP address of the VM after provisioning
|
|
||||||
output "vm_ip" {
|
output "vm_ip" {
|
||||||
value = proxmox_virtual_environment_vm.vm.ipv4_addresses[1][0] # Second network interface's first IP
|
value = proxmox_virtual_environment_vm.vm.ipv4_addresses[1][0]
|
||||||
description = "VM IP"
|
description = "VM IP"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,13 +26,10 @@ variable "vm_user" {
|
|||||||
default = "vez"
|
default = "vez"
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "vm_user_sshkeys" {
|
variable "vm_user_sshkey" {
|
||||||
description = "Admin user SSH keys of the VM"
|
description = "Admin user SSH key of the VM"
|
||||||
type = list(string)
|
type = string
|
||||||
default = [
|
default = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID62LmYRu1rDUha3timAIcA39LtcIOny1iAgFLnxoBxm vez@bastion"
|
||||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID62LmYRu1rDUha3timAIcA39LtcIOny1iAgFLnxoBxm vez@bastion",
|
|
||||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHovfHKpqTvwj5zrcSuSZALa8iiH6qBvE5dyJCz9eQ2k vez@surface"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "vm_cpu" {
|
variable "vm_cpu" {
|
||||||
@@ -47,12 +44,6 @@ variable "vm_ram" {
|
|||||||
default = 2048
|
default = 2048
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "vm_disk_size" {
|
|
||||||
description = "Size of the disk (GB) of the VM"
|
|
||||||
type = number
|
|
||||||
default = 10
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "vm_bios" {
|
variable "vm_bios" {
|
||||||
description = "Type of BIOS used for the VM"
|
description = "Type of BIOS used for the VM"
|
||||||
type = string
|
type = string
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ locals {
|
|||||||
for node in data.proxmox_virtual_environment_nodes.pve_nodes.names : [
|
for node in data.proxmox_virtual_environment_nodes.pve_nodes.names : [
|
||||||
for role, config in local.vm_attr : {
|
for role, config in local.vm_attr : {
|
||||||
node_name = node
|
node_name = node
|
||||||
vm_name = "${node}-${role}"
|
vm_name = "${role}-${node}"
|
||||||
vm_cpu = config.cpu
|
vm_cpu = config.cpu
|
||||||
vm_ram = config.ram
|
vm_ram = config.ram
|
||||||
vm_vlan = config.vlan
|
vm_vlan = config.vlan
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
module "pve_vm" {
|
|
||||||
source = "../../modules/pve_vm"
|
|
||||||
for_each = local.vm_list
|
|
||||||
|
|
||||||
node_name = each.value.node_name
|
|
||||||
vm_name = each.value.vm_name
|
|
||||||
vm_cpu = each.value.vm_cpu
|
|
||||||
vm_ram = each.value.vm_ram
|
|
||||||
vm_vlan = each.value.vm_vlan
|
|
||||||
}
|
|
||||||
|
|
||||||
locals {
|
|
||||||
# Ordered list of VM hostnames
|
|
||||||
sem_hosts = ["sem01", "sem02", "sem03"]
|
|
||||||
|
|
||||||
# Create a map: host -> node
|
|
||||||
vm_list = {
|
|
||||||
for idx, host in local.sem_hosts :
|
|
||||||
host => {
|
|
||||||
node_name = data.proxmox_virtual_environment_nodes.pve_nodes.names[idx]
|
|
||||||
vm_name = host
|
|
||||||
vm_cpu = 1
|
|
||||||
vm_ram = 2048
|
|
||||||
vm_vlan = 66
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data "proxmox_virtual_environment_nodes" "pve_nodes" {}
|
|
||||||
|
|
||||||
output "vm_ip" {
|
|
||||||
value = { for k, v in module.pve_vm : k => v.vm_ip }
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
terraform {
|
|
||||||
required_providers {
|
|
||||||
proxmox = {
|
|
||||||
source = "bpg/proxmox"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
provider "proxmox" {
|
|
||||||
endpoint = var.proxmox_endpoint
|
|
||||||
api_token = var.proxmox_api_token
|
|
||||||
insecure = false
|
|
||||||
ssh {
|
|
||||||
agent = false
|
|
||||||
# private_key = file("~/.ssh/id_ed25519")
|
|
||||||
username = var.proxmox_ssh_username
|
|
||||||
password = var.proxmox_ssh_password
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
variable "proxmox_endpoint" {
|
|
||||||
description = "Proxmox URL endpoint"
|
|
||||||
type = string
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "proxmox_api_token" {
|
|
||||||
description = "Proxmox API token"
|
|
||||||
type = string
|
|
||||||
sensitive = true
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "proxmox_ssh_username" {
|
|
||||||
description = "Proxmox SSH username"
|
|
||||||
type = string
|
|
||||||
sensitive = true
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "proxmox_ssh_password" {
|
|
||||||
description = "Proxmox SSH password"
|
|
||||||
type = string
|
|
||||||
sensitive = true
|
|
||||||
}
|
|
||||||
@@ -7,4 +7,4 @@ variable "proxmox_api_token" {
|
|||||||
description = "Proxmox API token"
|
description = "Proxmox API token"
|
||||||
type = string
|
type = string
|
||||||
sensitive = true
|
sensitive = true
|
||||||
}
|
}
|
||||||
@@ -83,7 +83,8 @@ resource "proxmox_virtual_environment_vm" "vm" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
network_device {
|
network_device {
|
||||||
bridge = "vlan${var.vm_vlan}" # VNet used with VLAN ID
|
bridge = "vmbr0" # Use the default bridge
|
||||||
|
vlan_id = var.vm_vlan # VLAN tagging if used
|
||||||
}
|
}
|
||||||
|
|
||||||
operating_system {
|
operating_system {
|
||||||
|
|||||||
Reference in New Issue
Block a user