Initial commit

This commit is contained in:
Francesco Zimbolo
2026-04-23 07:49:11 +00:00
commit 57ddf083f7
19 changed files with 802 additions and 0 deletions

153
ansible/deploy.yml Normal file
View File

@@ -0,0 +1,153 @@
---
- name: Converged Proxmox & Optional Linstor Deployment
hosts: proxmox
become: true
gather_facts: true
any_errors_fatal: true
vars:
corosync_ip: "{{ priv_ip | default(ansible_host) }}"
master_corosync_ip: "{{ hostvars[groups['proxmox'][0]]['priv_ip'] | default(hostvars[groups['proxmox'][0]]['ansible_host']) }}"
is_master: "{{ inventory_hostname == groups['proxmox'][0] }}"
tasks:
# ==========================================
# PHASE 1: REPOSITORIES & LICENSING
# ==========================================
- name: Apply Proxmox Enterprise Key
ansible.builtin.command: pvesubscription set -k {{ pve_enterprise_key }}
when: pve_enterprise_key | length > 0
register: pve_key_result
changed_when: "'successfully' in pve_key_result.stdout"
- name: Manage Proxmox Enterprise Repository
ansible.builtin.lineinfile:
path: /etc/apt/sources.list.d/pve-enterprise.list
regexp: '^#?deb https://enterprise.proxmox.com/debian/pve'
line: "{{ '' if pve_enterprise_key | length > 0 else '#' }}deb https://enterprise.proxmox.com/debian/pve bookworm pve-enterprise"
state: present
- name: Manage Proxmox No-Subscription Repository
ansible.builtin.apt_repository:
repo: 'deb http://download.proxmox.com/debian/pve bookworm pve-no-subscription'
state: "{{ 'absent' if pve_enterprise_key | length > 0 else 'present' }}"
filename: pve-no-subscription
- name: Add Linstor Repositories
ansible.builtin.include_role:
name: linstor_repo
when: deploy_linstor | bool
# ==========================================
# PHASE 2: UPDATES & NAG REMOVAL
# ==========================================
- name: Update APT cache and run dist-upgrade
ansible.builtin.apt:
update_cache: yes
upgrade: dist
autoremove: yes
register: apt_upgrade
- name: Remove Proxmox Subscription Nag (JS Patch)
ansible.builtin.replace:
path: /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js
regexp: "(Ext.Msg.show\\(\\{\\s+title: gettext\\('No valid subscription'\\),)"
replace: "void({ //\\1"
notify: Restart pveproxy
- name: Install Linstor Packages
ansible.builtin.include_role:
name: linstor_install
when: deploy_linstor | bool
- name: Reboot if kernel was updated
ansible.builtin.reboot:
msg: "Rebooting to apply new PVE Kernel updates"
when: apt_upgrade.changed
# ==========================================
# PHASE 3: PROXMOX CLUSTERING
# ==========================================
- name: Ensure SSH keypair exists for root
ansible.builtin.user:
name: root
generate_ssh_key: yes
ssh_key_bits: 4096
- name: Fetch public SSH keys from all nodes
ansible.builtin.slurp:
src: /root/.ssh/id_rsa.pub
register: ssh_pub_keys
- name: Distribute SSH keys to build full-mesh trust
ansible.posix.authorized_key:
user: root
key: "{{ hostvars[item]['ssh_pub_keys']['content'] | b64decode }}"
state: present
loop: "{{ ansible_play_hosts }}"
- name: Automatically accept host keys for cluster subnets
ansible.builtin.blockinfile:
path: /root/.ssh/config
create: yes
mode: '0600'
block: |
Host {{ master_corosync_ip | regex_replace('\.[0-9]+$', '.*') }} {{ hostvars[groups['proxmox'][0]]['ansible_host'] | regex_replace('\.[0-9]+$', '.*') }}
StrictHostKeyChecking no
UserKnownHostsFile=/dev/null
- name: Check current Proxmox cluster status
ansible.builtin.command: pvecm status
register: pvecm_status
ignore_errors: yes
changed_when: false
- name: Initialize the Proxmox Cluster (Master Node)
ansible.builtin.command: pvecm create {{ pve_cluster_name }} --link0 {{ corosync_ip }}
when:
- pvecm_status.rc != 0
- is_master | bool
- name: Wait for Corosync to stabilize
ansible.builtin.pause:
seconds: 10
when:
- pvecm_status.rc != 0
- is_master | bool
- name: Join the Proxmox Cluster (Worker Nodes)
ansible.builtin.command: >
pvecm add {{ master_corosync_ip }} --link0 {{ corosync_ip }} --use_ssh yes
when:
- pvecm_status.rc != 0
- not is_master | bool
async: 60
poll: 10
# ==========================================
# PHASE 4: LINSTOR CONFIGURATION
# ==========================================
- name: Configure Linstor (Skipped if deploy_linstor=false)
when: deploy_linstor | bool
block:
- name: Configure Linstor Cluster
ansible.builtin.include_role:
name: linstor_cluster
- name: Configure Linstor Storage
ansible.builtin.include_role:
name: linstor_storage
- name: Configure Linstor HA
ansible.builtin.include_role:
name: linstor_ha
- name: Configure Proxmox Storage Plugin for Linstor
ansible.builtin.include_role:
name: proxmox_storage
handlers:
- name: Restart pveproxy
ansible.builtin.service:
name: pveproxy
state: restarted

View File

@@ -0,0 +1,37 @@
---
# Deployment Toggles
deploy_linstor: false # Set to true to install and configure Linstor/DRBD
# Proxmox Cluster Configuration
pve_cluster_name: "prod-cluster"
pve_enterprise_key: "" # Leave empty ("") to use no-subscription, or insert "PVE..." key
# High Availability Controller Configuration
ha_pool: "fast_pool_1"
ha_vip: "10.191.125.4"
ha_vip_cidr: "29"
# Linstor Storage Pools Configuration
linstor_storage_pools:
- pool_name: "fast_pool_1"
rg_name: "fast_pool_1"
vg_name: "pve"
thin_name: "data"
# TARGET RULES:
# Use raw disk (/dev/sdb) to wipe/create LVM
# Use existing thin-pool path (pve/data) to register safely
targets:
pve-node-01: "pve/data"
pve-node-02: "pve/data"
pve-node-03: "pve/data"
- pool_name: "slow_pool_1"
rg_name: "slow_pool_1"
vg_name: "sp1"
thin_name: "data"
# TARGET RULES:
# Use raw disk (/dev/sdb) to wipe/create LVM
# Use existing thin-pool path (pve/data) to register safely
targets:
pve-node-01: "/dev/sda"
pve-node-02: "/dev/sda"
pve-node-03: "/dev/sda"

View File

@@ -0,0 +1,13 @@
all:
children:
proxmox:
hosts:
pve-node-01:
ansible_host: 10.191.25.21
priv_ip: 10.191.125.1
pve-node-02:
ansible_host: 10.191.25.22
priv_ip: 10.191.125.2
pve-node-03:
ansible_host: 10.191.25.23
priv_ip: 10.191.125.3

View File

@@ -0,0 +1,61 @@
---
# tasks file for linstor_cluster
- name: Safely manage LINSTOR cluster nodes in /etc/hosts
ansible.builtin.blockinfile:
path: /etc/hosts
marker: "# {mark} ANSIBLE MANAGED BLOCK - LINSTOR CLUSTER"
block: |
# Linstor Cluster Nodes (Primary: Private Network)
{% for host in groups['proxmox'] %}
{{ hostvars[host].priv_ip }} {{ host }}
{% endfor %}
# Linstor Cluster Nodes (Fallback: Public Network)
{% for host in groups['proxmox'] %}
{{ hostvars[host].ansible_host }} {{ host }}-public
{% endfor %}
- name: Ensure /etc/linstor directory exists
ansible.builtin.file:
path: /etc/linstor
state: directory
mode: '0755'
- name: Deploy linstor-client.conf
ansible.builtin.template:
src: linstor-client.conf.j2
dest: /etc/linstor/linstor-client.conf
mode: '0644'
# ---------------------------------------------------------
# LINSTOR NODE REGISTRATION (Executed ONLY on Node 1)
# ---------------------------------------------------------
- name: Register nodes in LINSTOR
ansible.builtin.shell: |
set -o pipefail
if ! linstor node list | grep -q " {{ item }} "; then
linstor node create "{{ item }}" "{{ hostvars[item].priv_ip }}" --node-type Combined > /dev/null
echo "changed"
fi
args:
executable: /bin/bash
register: linstor_cluster_node_create
changed_when: "'changed' in linstor_cluster_node_create.stdout"
loop: "{{ groups['proxmox'] }}"
when: inventory_hostname == groups['proxmox'][0]
- name: Configure Multipath Fallback Interfaces
ansible.builtin.shell: |
set -o pipefail
if ! linstor node interface list "{{ item }}" | grep -q " fallback "; then
linstor node interface create "{{ item }}" fallback "{{ hostvars[item].ansible_host }}" > /dev/null
echo "changed"
fi
args:
executable: /bin/bash
register: linstor_cluster_int_create
changed_when: "'changed' in linstor_cluster_int_create.stdout"
loop: "{{ groups['proxmox'] }}"
when: inventory_hostname == groups['proxmox'][0]

View File

@@ -0,0 +1,3 @@
[global]
# Ansible dynamically grabs all hosts in the 'proxmox' group and joins them with commas
controllers={{ groups['proxmox'] | join(',') }}

View File

@@ -0,0 +1,134 @@
---
# tasks file for linstor_ha
# ---------------------------------------------------------
# STEP 1: CREATE HA RESOURCES (Executed ONLY on Node 1)
# ---------------------------------------------------------
- name: Create HA Resource Group and Spawn Database Volume
ansible.builtin.shell: |
set -o pipefail
if ! linstor resource-group list | grep -q " linstor-db-grp "; then
linstor resource-group create --storage-pool "{{ ha_pool }}" --place-count 3 --diskless-on-remaining true linstor-db-grp > /dev/null
linstor resource-group drbd-options --auto-promote=no --quorum=majority \
--on-suspended-primary-outdated=force-secondary --on-no-quorum=io-error --on-no-data-accessible=io-error linstor-db-grp > /dev/null
linstor volume-group create linstor-db-grp > /dev/null
linstor resource-group spawn-resources linstor-db-grp linstor_db 200M > /dev/null
echo "changed"
fi
args:
executable: /bin/bash
register: linstor_ha_rg_create
changed_when: "'changed' in linstor_ha_rg_create.stdout"
when: inventory_hostname == groups['proxmox'][0]
- name: Wait for DRBD block device to appear locally
ansible.builtin.wait_for:
path: /dev/drbd/by-res/linstor_db/0
state: present
timeout: 60
when: inventory_hostname == groups['proxmox'][0]
# ---------------------------------------------------------
# STEP 2: PREPARE ALL NODES
# ---------------------------------------------------------
- name: Install resource-agents
ansible.builtin.apt:
name: resource-agents
state: present
- name: Ensure local Linstor Controller is stopped and disabled
ansible.builtin.systemd:
name: linstor-controller
enabled: false
state: stopped
- name: Deploy var-lib-linstor.mount unit
ansible.builtin.copy:
dest: /etc/systemd/system/var-lib-linstor.mount
content: |
[Unit]
Description=Filesystem for the LINSTOR controller
[Mount]
What=/dev/drbd/by-res/linstor_db/0
Where=/var/lib/linstor
mode: '0644'
- name: Ensure drbd-reactor directory exists
ansible.builtin.file:
path: /etc/drbd-reactor.d
state: directory
mode: '0755'
- name: Deploy DRBD Reactor TOML config
ansible.builtin.template:
src: linstor_db.toml.j2
dest: /etc/drbd-reactor.d/linstor_db.toml
mode: '0644'
# Notice how the stat task is gone, replaced by 'creates' and 'removes'
- name: Move existing local database to .orig safely
ansible.builtin.command: mv /var/lib/linstor /var/lib/linstor.orig
args:
creates: /var/lib/linstor.orig
removes: /var/lib/linstor
- name: Create empty mount point and set immutable flag (+i)
ansible.builtin.file:
path: /var/lib/linstor
state: directory
mode: '0755'
attributes: '+i'
- name: Ensure Satellite Override directory exists
ansible.builtin.file:
path: /etc/systemd/system/linstor-satellite.service.d
state: directory
mode: '0755'
- name: Deploy Satellite Keep-Resource Override
ansible.builtin.copy:
dest: /etc/systemd/system/linstor-satellite.service.d/override.conf
content: |
[Service]
Environment=LS_KEEP_RES=linstor_db
mode: '0644'
- name: Reload daemon and restart Satellite
ansible.builtin.systemd:
daemon_reload: true
name: linstor-satellite
state: restarted
# ---------------------------------------------------------
# STEP 3: FORMAT & MIGRATE DB (Executed ONLY on Node 1)
# ---------------------------------------------------------
- name: Format HA Volume and Migrate Database
ansible.builtin.shell: |
set -o pipefail
if ! blkid /dev/drbd/by-res/linstor_db/0 | grep -q "ext4"; then
drbdadm primary linstor_db
mkfs.ext4 /dev/drbd/by-res/linstor_db/0 > /dev/null
chattr -i /var/lib/linstor
mount /dev/drbd/by-res/linstor_db/0 /var/lib/linstor
cp -a /var/lib/linstor.orig/* /var/lib/linstor/
umount /var/lib/linstor
chattr +i /var/lib/linstor
drbdadm secondary linstor_db
echo "changed"
fi
args:
executable: /bin/bash
register: linstor_ha_migrate
changed_when: "'changed' in linstor_ha_migrate.stdout"
when: inventory_hostname == groups['proxmox'][0]
# ---------------------------------------------------------
# STEP 4: ACTIVATE HA REACTOR
# ---------------------------------------------------------
- name: Enable and start DRBD Reactor
ansible.builtin.systemd:
name: drbd-reactor
enabled: true
state: started

View File

@@ -0,0 +1,9 @@
[[promoter]]
[promoter.resources.linstor_db]
start = [
"ocf:heartbeat:IPaddr2 service_ip cidr_netmask={{ ha_vip_cidr }} ip={{ ha_vip }}",
"var-lib-linstor.mount",
"linstor-controller.service"
]
on-drbd-demote-failure = "reboot"
secondary-force = true

View File

@@ -0,0 +1,63 @@
---
# tasks file for linstor_install
- name: Install Proxmox kernel headers
ansible.builtin.apt:
name:
- "proxmox-headers-{{ ansible_facts['kernel'] }}"
- proxmox-default-headers
state: present
update_cache: true
- name: Install LINSTOR, DRBD, and LVM dependencies
ansible.builtin.apt:
name:
- drbd-dkms
- drbd-utils
- lvm2
- thin-provisioning-tools
- linstor-satellite
- linstor-proxmox
- linstor-controller
- linstor-client
- drbd-reactor
state: present
- name: Check currently loaded DRBD version
ansible.builtin.shell: cat /proc/drbd || true
register: linstor_install_drbd_version
changed_when: false
- name: Eject legacy DRBD 8.4 module if present
community.general.modprobe:
name: drbd
state: absent
when: "'version: 8' in linstor_install_drbd_version.stdout"
- name: Ensure DRBD kernel module is loaded
community.general.modprobe:
name: drbd
state: present
- name: Enable and start base services (Satellite & Reactor)
ansible.builtin.systemd:
name: "{{ item }}"
enabled: true
state: started
loop:
- linstor-satellite
- drbd-reactor
- name: Ensure LINSTOR Controller is disabled safely on all nodes
ansible.builtin.systemd:
name: linstor-controller
enabled: false
state: stopped
- name: Enable and start LINSTOR Controller strictly on Node 1
ansible.builtin.systemd:
name: linstor-controller
enabled: true
state: started
# Ansible Magic: This dynamically targets the very first node listed in your inventory under the 'proxmox' group!
when: inventory_hostname == groups['proxmox'][0]

View File

@@ -0,0 +1,24 @@
---
# tasks file for linstor_repo
- name: Download LINBIT keyring package
ansible.builtin.get_url:
url: https://packages.linbit.com/public/linbit-keyring.deb
dest: /tmp/linbit-keyring.deb
mode: '0644'
- name: Install LINBIT keyring
ansible.builtin.apt:
deb: /tmp/linbit-keyring.deb
state: present
- name: Add LINBIT APT repository for Proxmox
ansible.builtin.apt_repository:
repo: >-
deb [signed-by=/etc/apt/trusted.gpg.d/linbit-keyring.gpg]
https://packages.linbit.com/public/
proxmox-{{ ansible_facts['distribution_major_version'] | int - 4 }}
drbd-9
filename: linbit
state: present
update_cache: true

View File

@@ -0,0 +1,69 @@
---
# tasks file for linstor_storage
- name: Configure LVM global_filter for DRBD safely
ansible.builtin.lineinfile:
path: /etc/lvm/lvm.conf
insertafter: '^[ \t]*devices[ \t]*\{'
# This regex ensures we only ever have ONE DRBD filter line, replacing the old one if it exists
regexp: '^[ \t]*global_filter[ \t]*=.*r\|\^/dev/drbd\|'
line: ' global_filter = [ "r|^/dev/drbd|", "r|^/dev/mapper/[lL]instor|" ]'
state: present
- name: Ensure Volume Groups exist for raw disks
community.general.lvg:
vg: "{{ item.vg_name }}"
pvs: "{{ item.targets[inventory_hostname] }}"
loop: "{{ linstor_storage_pools }}"
# Jinja2 magic: Only run this LVM task if the target string starts with '/dev/'
when: item.targets[inventory_hostname].startswith('/dev/')
- name: Ensure LVM Thin Pools exist for raw disks
community.general.lvol:
vg: "{{ item.vg_name }}"
lv: "{{ item.thin_name }}"
size: "100%FREE"
opts: "-T -Zn"
shrink: false
loop: "{{ linstor_storage_pools }}"
when: item.targets[inventory_hostname].startswith('/dev/')
# ---------------------------------------------------------
# LINSTOR STORAGE REGISTRATION (Executed ONLY on Node 1)
# ---------------------------------------------------------
- name: Register Storage Pools in LINSTOR
ansible.builtin.shell: |
set -o pipefail
# We use Jinja2 to write a custom bash loop for exactly the nodes in our inventory
{% for host in groups['proxmox'] %}
{% set target = item.targets[host] %}
{% set lvm_path = (item.vg_name + '/' + item.thin_name) if target.startswith('/dev/') else target %}
if ! linstor storage-pool list | grep -q " {{ host }} .* {{ item.pool_name }} "; then
linstor storage-pool create lvmthin "{{ host }}" "{{ item.pool_name }}" "{{ lvm_path }}" > /dev/null
echo "changed"
fi
{% endfor %}
args:
executable: /bin/bash
register: linstor_storage_pool_create
changed_when: "'changed' in linstor_storage_pool_create.stdout"
loop: "{{ linstor_storage_pools }}"
when: inventory_hostname == groups['proxmox'][0]
- name: Create Resource Groups and Volume Groups
ansible.builtin.shell: |
set -o pipefail
if ! linstor resource-group list | grep -q " {{ item.rg_name }} "; then
linstor resource-group create "{{ item.rg_name }}" --storage-pool "{{ item.pool_name }}" --place-count 3 > /dev/null
linstor volume-group create "{{ item.rg_name }}" > /dev/null
echo "changed"
fi
args:
executable: /bin/bash
register: linstor_storage_rg_create
changed_when: "'changed' in linstor_storage_rg_create.stdout"
loop: "{{ linstor_storage_pools }}"
when: inventory_hostname == groups['proxmox'][0]

View File

@@ -0,0 +1,29 @@
---
# tasks file for proxmox_storage
- name: Create a safety backup of Proxmox storage.cfg
ansible.builtin.copy:
src: /etc/pve/storage.cfg
dest: "/root/pve_storage.cfg.bak.{{ ansible_facts['date_time']['epoch'] }}"
remote_src: true
mode: '0600'
when: inventory_hostname == groups['proxmox'][0]
- name: Register LINSTOR resource groups in Proxmox GUI (pvesm)
ansible.builtin.shell: |
set -o pipefail
# We use the ha_vip variable, but gracefully fallback to Node 1's public IP if it's undefined
CONTROLLER="{{ ha_vip | default(hostvars[groups['proxmox'][0]].ansible_host) }}"
STORAGE_ID="ls-{{ item.rg_name }}"
if ! pvesm status | grep -q "^${STORAGE_ID} "; then
pvesm add drbd "${STORAGE_ID}" --controller "${CONTROLLER}" --resourcegroup "{{ item.rg_name }}" --content images,rootdir > /dev/null
echo "changed"
fi
args:
executable: /bin/bash
register: proxmox_storage_pvesm_add
changed_when: "'changed' in proxmox_storage_pvesm_add.stdout"
loop: "{{ linstor_storage_pools }}"
when: inventory_hostname == groups['proxmox'][0]