Refactoring + final touches
This commit is contained in:
15
ansible/ansible.cfg
Normal file
15
ansible/ansible.cfg
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[defaults]
|
||||||
|
remote_user = root
|
||||||
|
forks = 20
|
||||||
|
pipelining = True
|
||||||
|
result_format = yaml
|
||||||
|
bin_ansible_callbacks = True
|
||||||
|
callback_whitelist = profile_tasks
|
||||||
|
|
||||||
|
# SSH connection timeout (opening the connection)
|
||||||
|
timeout = 120
|
||||||
|
|
||||||
|
[ssh_connection]
|
||||||
|
ssh_args = -o ServerAliveInterval=60 -o ServerAliveCountMax=10 -o ControlMaster=auto -o ControlPersist=60s
|
||||||
|
# Timeout for individual commands on remote host (apt upgrade, DRBD, etc.)
|
||||||
|
command_timeout = 3600
|
||||||
@@ -8,16 +8,44 @@
|
|||||||
vars:
|
vars:
|
||||||
# Dynamically detect if we are in Single-Node or Cluster mode
|
# Dynamically detect if we are in Single-Node or Cluster mode
|
||||||
is_single_node: "{{ groups['proxmox'] | length == 1 }}"
|
is_single_node: "{{ groups['proxmox'] | length == 1 }}"
|
||||||
|
# standalone_mode can be set manually in group_vars to bypass cluster/linstor tasks
|
||||||
# Existing clustering variables
|
# even with multiple nodes in inventory (e.g. independent nodes for the same customer)
|
||||||
|
_skip_cluster: "{{ (is_single_node | bool) or (standalone_mode | default(false) | bool) }}"
|
||||||
|
|
||||||
|
# Corosync clustering IPs
|
||||||
corosync_ip: "{{ priv_ip | default(ansible_host) }}"
|
corosync_ip: "{{ priv_ip | default(ansible_host) }}"
|
||||||
master_corosync_ip: "{{ hostvars[groups['proxmox'][0]]['priv_ip'] | default(hostvars[groups['proxmox'][0]]['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] }}"
|
is_master: "{{ inventory_hostname == groups['proxmox'][0] }}"
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
|
# ==========================================
|
||||||
|
# PHASE 0: P2P PRIVATE NETWORK
|
||||||
|
# ==========================================
|
||||||
|
- name: Configure P2P Private Network Bridge
|
||||||
|
ansible.builtin.include_role:
|
||||||
|
name: p2p_network
|
||||||
|
when:
|
||||||
|
- not _skip_cluster | bool
|
||||||
|
- priv_nic_1 is defined
|
||||||
|
- priv_nic_2 is defined
|
||||||
|
- priv_ip is defined
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# PHASE 1: REPOSITORIES & LICENSING (PVE 9 / Trixie)
|
# PHASE 1: REPOSITORIES & LICENSING (PVE 9 / Trixie)
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
- name: Disable IPv6 system-wide
|
||||||
|
ansible.posix.sysctl:
|
||||||
|
name: "{{ item }}"
|
||||||
|
value: "1"
|
||||||
|
state: present
|
||||||
|
sysctl_file: /etc/sysctl.d/99-disable-ipv6.conf
|
||||||
|
reload: true
|
||||||
|
loop:
|
||||||
|
- net.ipv6.conf.all.disable_ipv6
|
||||||
|
- net.ipv6.conf.default.disable_ipv6
|
||||||
|
- net.ipv6.conf.lo.disable_ipv6
|
||||||
|
when: disable_ipv6 | default(false) | bool
|
||||||
|
|
||||||
- name: Apply Proxmox Enterprise Key
|
- name: Apply Proxmox Enterprise Key
|
||||||
ansible.builtin.command: pvesubscription set -k {{ pve_enterprise_key }}
|
ansible.builtin.command: pvesubscription set -k {{ pve_enterprise_key }}
|
||||||
when: pve_enterprise_key | default('') | length > 0
|
when: pve_enterprise_key | default('') | length > 0
|
||||||
@@ -53,27 +81,36 @@
|
|||||||
signed_by: /usr/share/keyrings/proxmox-archive-keyring.gpg
|
signed_by: /usr/share/keyrings/proxmox-archive-keyring.gpg
|
||||||
state: "{{ 'absent' if pve_enterprise_key | default('') | length > 0 else 'present' }}"
|
state: "{{ 'absent' if pve_enterprise_key | default('') | length > 0 else 'present' }}"
|
||||||
|
|
||||||
- name: Remove Ceph Enterprise Repository (Unused in Linstor)
|
- name: Remove Ceph Enterprise Repository (Unused in Linstor setups)
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: /etc/apt/sources.list.d/ceph.sources
|
path: /etc/apt/sources.list.d/ceph.sources
|
||||||
state: absent
|
state: absent
|
||||||
when: pve_enterprise_key | default('') | length == 0
|
when: pve_enterprise_key | default('') | length == 0
|
||||||
|
|
||||||
- name: Add Linstor Repositories (Skipped in Standalone Node)
|
- name: Add Linstor Repositories
|
||||||
ansible.builtin.include_role:
|
ansible.builtin.include_role:
|
||||||
name: linstor_repo
|
name: linstor_repo
|
||||||
when:
|
when:
|
||||||
- deploy_linstor | bool
|
- deploy_linstor | bool
|
||||||
- not is_standalone | bool
|
- not _skip_cluster | bool
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# PHASE 2: UPDATES & NAG REMOVAL
|
# PHASE 2: UPDATES & NAG REMOVAL
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
|
# NOTE: async/poll used here to prevent SSH timeout during long dist-upgrade.
|
||||||
|
# The task is submitted asynchronously (up to 1h) and polled every 30s.
|
||||||
- name: Update APT cache and run dist-upgrade
|
- name: Update APT cache and run dist-upgrade
|
||||||
ansible.builtin.apt:
|
ansible.builtin.apt:
|
||||||
update_cache: yes
|
update_cache: true
|
||||||
upgrade: dist
|
upgrade: dist
|
||||||
autoremove: yes
|
autoremove: true
|
||||||
|
lock_timeout: 300 # Wait up to 5 min for dpkg lock from other apt processes
|
||||||
|
dpkg_options: 'force-confnew,force-confdef' # Non-interactive config file handling
|
||||||
|
environment:
|
||||||
|
DEBIAN_FRONTEND: noninteractive # Prevent debconf from blocking on prompts
|
||||||
|
async: 3600 # Allow up to 1 hour for the full upgrade
|
||||||
|
poll: 30 # Check progress every 30 seconds
|
||||||
|
|
||||||
- name: Remove Proxmox Subscription Nag (JS Patch)
|
- name: Remove Proxmox Subscription Nag (JS Patch)
|
||||||
ansible.builtin.replace:
|
ansible.builtin.replace:
|
||||||
@@ -82,12 +119,12 @@
|
|||||||
replace: "void({ //\\1"
|
replace: "void({ //\\1"
|
||||||
notify: Restart pveproxy
|
notify: Restart pveproxy
|
||||||
|
|
||||||
- name: Install Linstor Packages (Skipped in Single Node)
|
- name: Install Linstor Packages
|
||||||
ansible.builtin.include_role:
|
ansible.builtin.include_role:
|
||||||
name: linstor_install
|
name: linstor_install
|
||||||
when:
|
when:
|
||||||
- deploy_linstor | bool
|
- deploy_linstor | bool
|
||||||
- not is_single_node | bool
|
- not _skip_cluster | bool
|
||||||
|
|
||||||
- name: Check if a reboot is required by APT
|
- name: Check if a reboot is required by APT
|
||||||
ansible.builtin.stat:
|
ansible.builtin.stat:
|
||||||
@@ -97,19 +134,30 @@
|
|||||||
- name: Reboot the node gracefully
|
- name: Reboot the node gracefully
|
||||||
ansible.builtin.reboot:
|
ansible.builtin.reboot:
|
||||||
msg: "Rebooting node to apply new kernel/system updates"
|
msg: "Rebooting node to apply new kernel/system updates"
|
||||||
reboot_timeout: 600 # Wait up to 10 minutes for the server to return
|
reboot_timeout: 600
|
||||||
when: reboot_required_file.stat.exists
|
when: reboot_required_file.stat.exists
|
||||||
|
|
||||||
|
# Explicit barrier: ensure ALL nodes are back online before proceeding to Phase 3.
|
||||||
|
# Without this, any_errors_fatal would abort if one node reboots and another doesn't,
|
||||||
|
# causing them to arrive at Phase 3 at different times.
|
||||||
|
- name: Wait for all nodes to be reachable after (optional) reboot
|
||||||
|
ansible.builtin.wait_for_connection:
|
||||||
|
timeout: 300
|
||||||
|
sleep: 5
|
||||||
|
|
||||||
|
- name: Sync barrier — wait for all nodes before clustering phase
|
||||||
|
ansible.builtin.meta: clear_host_errors
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# PHASE 3: PROXMOX CLUSTERING
|
# PHASE 3: PROXMOX CLUSTERING
|
||||||
# ==========================================
|
# ==========================================
|
||||||
- name: PROXMOX CLUSTERING (Skipped in Single Node Mode)
|
- name: PROXMOX CLUSTERING (Skipped in Single Node / Standalone Mode)
|
||||||
when: not is_single_node | bool
|
when: not _skip_cluster | bool
|
||||||
block:
|
block:
|
||||||
- name: Ensure SSH keypair exists for root
|
- name: Ensure SSH keypair exists for root
|
||||||
ansible.builtin.user:
|
ansible.builtin.user:
|
||||||
name: root
|
name: root
|
||||||
generate_ssh_key: yes
|
generate_ssh_key: true
|
||||||
ssh_key_bits: 4096
|
ssh_key_bits: 4096
|
||||||
|
|
||||||
- name: Fetch public SSH keys from all nodes
|
- name: Fetch public SSH keys from all nodes
|
||||||
@@ -124,51 +172,63 @@
|
|||||||
state: present
|
state: present
|
||||||
loop: "{{ ansible_play_hosts }}"
|
loop: "{{ ansible_play_hosts }}"
|
||||||
|
|
||||||
|
- name: Compute SSH wildcard host patterns for cluster subnets
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
_corosync_subnet: "{{ master_corosync_ip | regex_replace('\\.[0-9]+$', '.*') }}"
|
||||||
|
_mgmt_subnet: >-
|
||||||
|
{{ hostvars[groups['proxmox'][0]]['ansible_host']
|
||||||
|
| regex_replace('\\.[0-9]+$', '.*') }}
|
||||||
|
|
||||||
- name: Automatically accept host keys for cluster subnets
|
- name: Automatically accept host keys for cluster subnets
|
||||||
ansible.builtin.blockinfile:
|
ansible.builtin.blockinfile:
|
||||||
path: /root/.ssh/config
|
path: /root/.ssh/config
|
||||||
create: yes
|
create: true
|
||||||
mode: '0600'
|
mode: '0600'
|
||||||
block: |
|
block: |
|
||||||
Host {{ master_corosync_ip | regex_replace('\.[0-9]+$', '.*') }} {{ hostvars[groups['proxmox'][0]]['ansible_host'] | regex_replace('\.[0-9]+$', '.*') }}
|
Host {{ _corosync_subnet }} {{ _mgmt_subnet }}
|
||||||
StrictHostKeyChecking no
|
StrictHostKeyChecking no
|
||||||
UserKnownHostsFile=/dev/null
|
UserKnownHostsFile=/dev/null
|
||||||
|
|
||||||
- name: Check current Proxmox cluster status
|
- name: Check current Proxmox cluster status
|
||||||
ansible.builtin.command: pvecm status
|
ansible.builtin.command: pvecm status
|
||||||
register: pvecm_status
|
register: pvecm_status
|
||||||
ignore_errors: yes
|
ignore_errors: true
|
||||||
changed_when: false
|
changed_when: false
|
||||||
|
|
||||||
- name: Initialize the Proxmox Cluster (Master Node)
|
- name: Initialize the Proxmox Cluster (Master Node)
|
||||||
ansible.builtin.command: pvecm create {{ pve_cluster_name }} --link0 {{ corosync_ip }}
|
ansible.builtin.command: pvecm create {{ pve_cluster_name }} --link0 {{ corosync_ip }}
|
||||||
when:
|
changed_when: true
|
||||||
|
when:
|
||||||
- pvecm_status.rc != 0
|
- pvecm_status.rc != 0
|
||||||
- is_master | bool
|
- is_master | bool
|
||||||
|
|
||||||
- name: Wait for Corosync to stabilize
|
- name: Wait for Corosync to stabilize
|
||||||
ansible.builtin.pause:
|
ansible.builtin.pause:
|
||||||
seconds: 10
|
seconds: 10
|
||||||
when:
|
when:
|
||||||
- pvecm_status.rc != 0
|
- pvecm_status.rc != 0
|
||||||
- is_master | bool
|
- is_master | bool
|
||||||
|
|
||||||
- name: Join the Proxmox Cluster (Worker Nodes)
|
- name: Join the Proxmox Cluster (Worker Nodes — one at a time)
|
||||||
ansible.builtin.command: >
|
ansible.builtin.command: >
|
||||||
pvecm add {{ master_corosync_ip }} --link0 {{ corosync_ip }} --use_ssh yes
|
pvecm add {{ master_corosync_ip }} --link0 {{ corosync_ip }} --use_ssh yes
|
||||||
when:
|
register: pvecm_join
|
||||||
|
changed_when: true
|
||||||
|
failed_when: pvecm_join.rc != 0 or 'TASK ERROR' in pvecm_join.stdout or 'TASK ERROR' in pvecm_join.stderr
|
||||||
|
when:
|
||||||
- pvecm_status.rc != 0
|
- pvecm_status.rc != 0
|
||||||
- not is_master | bool
|
- not is_master | bool
|
||||||
async: 60
|
throttle: 1 # Prevent race condition: pvecm add does not support parallel joins
|
||||||
|
async: 120
|
||||||
poll: 10
|
poll: 10
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# PHASE 4: LINSTOR CONFIGURATION
|
# PHASE 4: LINSTOR CONFIGURATION
|
||||||
# ==========================================
|
# ==========================================
|
||||||
- name: LINSTOR CONFIGURATION (Skipped if deploy_linstor=false or Single Node)
|
- name: LINSTOR CONFIGURATION (Skipped if deploy_linstor=false or Single Node / Standalone)
|
||||||
when:
|
when:
|
||||||
- deploy_linstor | bool
|
- deploy_linstor | bool
|
||||||
- not is_single_node | bool
|
- not _skip_cluster | bool
|
||||||
block:
|
block:
|
||||||
- name: Configure Linstor Cluster
|
- name: Configure Linstor Cluster
|
||||||
ansible.builtin.include_role:
|
ansible.builtin.include_role:
|
||||||
|
|||||||
70
ansible/inventories/_template/group_vars/all.yml
Normal file
70
ansible/inventories/_template/group_vars/all.yml
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
---
|
||||||
|
# ============================================================
|
||||||
|
# Proxmox Post-Install - Group Variables Template
|
||||||
|
# Copy this to your inventory folder as group_vars/all.yml
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
# --- Deployment Toggles ---
|
||||||
|
# standalone_mode: Set to true to skip ALL cluster and Linstor tasks,
|
||||||
|
# even when the inventory contains multiple nodes (useful for independent nodes per customer).
|
||||||
|
standalone_mode: false
|
||||||
|
|
||||||
|
# deploy_linstor: Set to true to install and configure Linstor + DRBD storage.
|
||||||
|
# Requires at least 3 nodes and standalone_mode: false.
|
||||||
|
deploy_linstor: false
|
||||||
|
|
||||||
|
# disable_ipv6: Set to true to disable IPv6 system-wide via sysctl.
|
||||||
|
# Useful when IPv6 is unrouted and causes connection timeouts (e.g. apt, curl).
|
||||||
|
disable_ipv6: false
|
||||||
|
|
||||||
|
# wipe_linstor_disks: Set to true to wipe partition tables and signatures from raw block devices
|
||||||
|
# before creating LVM VGs. Only affects targets defined as /dev/... paths.
|
||||||
|
# WARNING: destructive — only set on first deployment, never on a running cluster.
|
||||||
|
wipe_linstor_disks: false
|
||||||
|
|
||||||
|
# --- Proxmox Cluster ---
|
||||||
|
pve_cluster_name: "example-cluster"
|
||||||
|
# pve_enterprise_key: "" # Optional: global key (prefer per-host key in hosts.ini)
|
||||||
|
|
||||||
|
# --- Linstor / LINBIT Repository ---
|
||||||
|
# Auto-calculated as: Debian major version - 4 (e.g. Debian 13 = proxmox-9).
|
||||||
|
# Override manually if LINBIT publishes a differently-named repo for new Proxmox versions.
|
||||||
|
# linbit_proxmox_version: 9
|
||||||
|
|
||||||
|
# --- Linstor HA Controller ---
|
||||||
|
# ha_pool: Name of the Linstor storage pool to use for the controller HA volume.
|
||||||
|
ha_pool: "fast_pool_1"
|
||||||
|
# ha_vip: Virtual IP that follows the active Linstor controller across nodes.
|
||||||
|
ha_vip: "192.168.2.4"
|
||||||
|
# ha_vip_cidr: Prefix length for the VIP address (e.g. 24 for /24).
|
||||||
|
ha_vip_cidr: "24"
|
||||||
|
|
||||||
|
# --- Linstor Storage Pools ---
|
||||||
|
# Each entry defines one Resource Group, composed of one storage pool per node.
|
||||||
|
# All pools in the same entry share the same pool_name across nodes.
|
||||||
|
#
|
||||||
|
# TARGET RULES:
|
||||||
|
# - Use a raw block device path (/dev/sdb) to auto-create VG + LVM Thin Pool.
|
||||||
|
# - Use an existing thin-pool path (vg_name/thin_name, e.g. "pve/data") to register
|
||||||
|
# an already-existing LVM thin pool WITHOUT touching existing data.
|
||||||
|
#
|
||||||
|
# Keys under "targets" must match the Ansible inventory_hostname of each node
|
||||||
|
# (i.e. the left-hand label in hosts.ini, e.g. "pve-node-01").
|
||||||
|
linstor_storage_pools:
|
||||||
|
- pool_name: "fast_pool_1"
|
||||||
|
rg_name: "fast_pool_1"
|
||||||
|
vg_name: "pve"
|
||||||
|
thin_name: "data"
|
||||||
|
targets:
|
||||||
|
pve-node-01: "pve/data" # Register existing pve/data thin pool
|
||||||
|
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"
|
||||||
|
targets:
|
||||||
|
pve-node-01: "/dev/sdb" # Format /dev/sdb as VG sp1 + thin pool data
|
||||||
|
pve-node-02: "/dev/sdb"
|
||||||
|
pve-node-03: "/dev/sdb"
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
---
|
|
||||||
# Deployment Toggles
|
|
||||||
standalone_mode: true # Set to true to bypass all Corosync/Linstor tasks for independent nodes
|
|
||||||
deploy_linstor: false # Set to true to install and configure Linstor/DRBD
|
|
||||||
|
|
||||||
# Proxmox Cluster Configuration
|
|
||||||
pve_cluster_name: "example-cluster"
|
|
||||||
|
|
||||||
# High Availability Controller Configuration
|
|
||||||
ha_pool: "fast_pool_1"
|
|
||||||
ha_vip: "192.168.2.4"
|
|
||||||
ha_vip_cidr: "29"
|
|
||||||
|
|
||||||
# Linstor Storage Pools Configuration
|
|
||||||
# You can add or remove pools as needed
|
|
||||||
|
|
||||||
# TARGET RULES:
|
|
||||||
# Use raw disk (/dev/sdb) to wipe/create LVM
|
|
||||||
# Use existing thin-pool path (pve/data) to register safely (data won't be deleted)
|
|
||||||
linstor_storage_pools:
|
|
||||||
- pool_name: "fast_pool_1"
|
|
||||||
rg_name: "fast_pool_1"
|
|
||||||
vg_name: "pve"
|
|
||||||
thin_name: "data"
|
|
||||||
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"
|
|
||||||
targets:
|
|
||||||
pve-node-01: "/dev/sda"
|
|
||||||
pve-node-02: "/dev/sda"
|
|
||||||
pve-node-03: "/dev/sda"
|
|
||||||
25
ansible/inventories/_template/hosts.ini
Normal file
25
ansible/inventories/_template/hosts.ini
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# ============================================================
|
||||||
|
# Proxmox Cluster Inventory - INI Format
|
||||||
|
# Copy this file to your inventory folder as "hosts.ini"
|
||||||
|
# and adjust IPs, hostnames, and optional keys.
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
[proxmox]
|
||||||
|
# Format: <inventory_alias> ansible_host=<management_ip> [priv_ip=<cluster_ip>] [priv_nic_1=<nic>] [priv_nic_2=<nic>] [pve_enterprise_key=<key>]
|
||||||
|
#
|
||||||
|
# - inventory_alias : Arbitrary label used by Ansible. Does NOT need to match the real hostname.
|
||||||
|
# The real system hostname (ansible_hostname) is used for Proxmox & Linstor nodes.
|
||||||
|
# - ansible_host : IP used by Ansible to SSH into the node (management/public IP).
|
||||||
|
# - priv_ip : IP used for Corosync cluster traffic and Linstor replication (private/dedicated NIC).
|
||||||
|
# If omitted, ansible_host is used as fallback.
|
||||||
|
# - priv_nic_1/2 : (Optional) The two NICs forming the full-mesh p2p bridge (named "p2p").
|
||||||
|
# Required only when running network.yml to configure the private bridge.
|
||||||
|
# NIC names differ per node — check with: ip link show
|
||||||
|
# - pve_enterprise_key : (Optional) Per-node Proxmox enterprise subscription key.
|
||||||
|
|
||||||
|
pve-node-01 ansible_host=192.168.1.1 priv_ip=192.168.2.1 priv_nic_1=enp2s0 priv_nic_2=enp3s0
|
||||||
|
pve-node-02 ansible_host=192.168.1.2 priv_ip=192.168.2.2 priv_nic_1=enp2s0 priv_nic_2=enp3s0
|
||||||
|
pve-node-03 ansible_host=192.168.1.3 priv_ip=192.168.2.3 priv_nic_1=enp2s0 priv_nic_2=enp3s0
|
||||||
|
|
||||||
|
# Single-node example (no cluster, no Linstor):
|
||||||
|
# pve-standalone ansible_host=10.0.0.50
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
all:
|
|
||||||
children:
|
|
||||||
proxmox:
|
|
||||||
hosts:
|
|
||||||
pve-node-01:
|
|
||||||
ansible_host: 192.168.1.1
|
|
||||||
priv_ip: 192.168.2.1
|
|
||||||
pve_enterprise_key: "pve1c-111111111111"
|
|
||||||
pve-node-02:
|
|
||||||
ansible_host: 192.168.1.2
|
|
||||||
priv_ip: 192.168.2.1
|
|
||||||
pve_enterprise_key: "pve1c-222222222222"
|
|
||||||
pve-node-03:
|
|
||||||
ansible_host: 192.168.1.3
|
|
||||||
priv_ip: 192.168.2.3
|
|
||||||
pve_enterprise_key: "pve1c-333333333333"
|
|
||||||
14
ansible/network.yml
Normal file
14
ansible/network.yml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
- name: Configure P2P Private Network Bridge
|
||||||
|
hosts: proxmox
|
||||||
|
become: true
|
||||||
|
gather_facts: true
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Configure p2p bridge
|
||||||
|
ansible.builtin.include_role:
|
||||||
|
name: p2p_network
|
||||||
|
when:
|
||||||
|
- priv_nic_1 is defined
|
||||||
|
- priv_nic_2 is defined
|
||||||
|
- priv_ip is defined
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
# tasks file for linstor_cluster
|
# tasks file for linstor_cluster
|
||||||
|
# IMPORTANT: All Linstor node registrations use ansible_hostname (the real system hostname)
|
||||||
|
# to ensure they match Proxmox node names, which are also derived from the system hostname.
|
||||||
|
# The Ansible inventory_hostname (e.g. "pve1") may differ from the real hostname (e.g. "pve-node-01").
|
||||||
|
|
||||||
- name: Safely manage LINSTOR cluster nodes in /etc/hosts
|
- name: Safely manage LINSTOR cluster nodes in /etc/hosts
|
||||||
ansible.builtin.blockinfile:
|
ansible.builtin.blockinfile:
|
||||||
@@ -8,12 +11,12 @@
|
|||||||
block: |
|
block: |
|
||||||
# Linstor Cluster Nodes (Primary: Private Network)
|
# Linstor Cluster Nodes (Primary: Private Network)
|
||||||
{% for host in groups['proxmox'] %}
|
{% for host in groups['proxmox'] %}
|
||||||
{{ hostvars[host].priv_ip }} {{ host }}
|
{{ hostvars[host]['priv_ip'] | default(hostvars[host]['ansible_host']) }} {{ hostvars[host]['ansible_facts']['hostname'] }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
# Linstor Cluster Nodes (Fallback: Public Network)
|
# Linstor Cluster Nodes (Fallback: Public Network)
|
||||||
{% for host in groups['proxmox'] %}
|
{% for host in groups['proxmox'] %}
|
||||||
{{ hostvars[host].ansible_host }} {{ host }}-public
|
{{ hostvars[host]['ansible_host'] }} {{ hostvars[host]['ansible_facts']['hostname'] }}-public
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
- name: Ensure /etc/linstor directory exists
|
- name: Ensure /etc/linstor directory exists
|
||||||
@@ -30,13 +33,17 @@
|
|||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# LINSTOR NODE REGISTRATION (Executed ONLY on Node 1)
|
# LINSTOR NODE REGISTRATION (Executed ONLY on Node 1)
|
||||||
|
# Uses ansible_hostname (real system hostname) for Linstor node names
|
||||||
|
# to guarantee compatibility with Proxmox node names.
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
- name: Register nodes in LINSTOR
|
- name: Register nodes in LINSTOR
|
||||||
ansible.builtin.shell: |
|
ansible.builtin.shell: |
|
||||||
set -o pipefail
|
set -o pipefail
|
||||||
if ! linstor node list | grep -q " {{ item }} "; then
|
NODE_NAME="{{ hostvars[item]['ansible_facts']['hostname'] }}"
|
||||||
linstor node create "{{ item }}" "{{ hostvars[item].priv_ip }}" --node-type Combined > /dev/null
|
NODE_IP="{{ hostvars[item]['priv_ip'] | default(hostvars[item]['ansible_host']) }}"
|
||||||
|
if ! linstor node list | grep -q " ${NODE_NAME} "; then
|
||||||
|
linstor node create "${NODE_NAME}" "${NODE_IP}" --node-type Combined > /dev/null
|
||||||
echo "changed"
|
echo "changed"
|
||||||
fi
|
fi
|
||||||
args:
|
args:
|
||||||
@@ -49,8 +56,10 @@
|
|||||||
- name: Configure Multipath Fallback Interfaces
|
- name: Configure Multipath Fallback Interfaces
|
||||||
ansible.builtin.shell: |
|
ansible.builtin.shell: |
|
||||||
set -o pipefail
|
set -o pipefail
|
||||||
if ! linstor node interface list "{{ item }}" | grep -q " fallback "; then
|
NODE_NAME="{{ hostvars[item]['ansible_facts']['hostname'] }}"
|
||||||
linstor node interface create "{{ item }}" fallback "{{ hostvars[item].ansible_host }}" > /dev/null
|
NODE_PUBLIC_IP="{{ hostvars[item]['ansible_host'] }}"
|
||||||
|
if ! linstor node interface list "${NODE_NAME}" | grep -q " fallback "; then
|
||||||
|
linstor node interface create "${NODE_NAME}" fallback "${NODE_PUBLIC_IP}" > /dev/null
|
||||||
echo "changed"
|
echo "changed"
|
||||||
fi
|
fi
|
||||||
args:
|
args:
|
||||||
@@ -59,3 +68,22 @@
|
|||||||
changed_when: "'changed' in linstor_cluster_int_create.stdout"
|
changed_when: "'changed' in linstor_cluster_int_create.stdout"
|
||||||
loop: "{{ groups['proxmox'] }}"
|
loop: "{{ groups['proxmox'] }}"
|
||||||
when: inventory_hostname == groups['proxmox'][0]
|
when: inventory_hostname == groups['proxmox'][0]
|
||||||
|
|
||||||
|
- name: Wait for all Linstor satellite nodes to appear as Online
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
set -o pipefail
|
||||||
|
if linstor node list | sed 's/\x1b\[[0-9;]*m//g' | grep " {{ hostvars[item]['ansible_facts']['hostname'] }} " | grep -q "Online"; then
|
||||||
|
echo "online"
|
||||||
|
else
|
||||||
|
echo "not_ready"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
register: linstor_cluster_node_status
|
||||||
|
until: "'online' in linstor_cluster_node_status.stdout"
|
||||||
|
retries: 12
|
||||||
|
delay: 10
|
||||||
|
changed_when: false
|
||||||
|
loop: "{{ groups['proxmox'] }}"
|
||||||
|
when: inventory_hostname == groups['proxmox'][0]
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
[global]
|
[global]
|
||||||
# Ansible dynamically grabs all hosts in the 'proxmox' group and joins them with commas
|
# Controllers list uses private IPs of all nodes (tries each in order).
|
||||||
controllers={{ groups['proxmox'] | join(',') }}
|
# After linstor_ha setup, the VIP becomes the active controller endpoint.
|
||||||
|
# The drbd-reactor automatically manages controller failover via the VIP.
|
||||||
|
controllers={% for host in groups['proxmox'] %}{{ hostvars[host]['priv_ip'] | default(hostvars[host]['ansible_host']) }}{% if not loop.last %},{% endif %}{% endfor %}
|
||||||
|
|||||||
5
ansible/roles/linstor_ha/handlers/main.yml
Normal file
5
ansible/roles/linstor_ha/handlers/main.yml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
- name: Reload drbd-reactor
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: drbd-reactor
|
||||||
|
state: reloaded
|
||||||
@@ -65,6 +65,7 @@
|
|||||||
src: linstor_db.toml.j2
|
src: linstor_db.toml.j2
|
||||||
dest: /etc/drbd-reactor.d/linstor_db.toml
|
dest: /etc/drbd-reactor.d/linstor_db.toml
|
||||||
mode: '0644'
|
mode: '0644'
|
||||||
|
notify: Reload drbd-reactor
|
||||||
|
|
||||||
# Notice how the stat task is gone, replaced by 'creates' and 'removes'
|
# Notice how the stat task is gone, replaced by 'creates' and 'removes'
|
||||||
- name: Move existing local database to .orig safely
|
- name: Move existing local database to .orig safely
|
||||||
|
|||||||
4
ansible/roles/linstor_repo/handlers/main.yml
Normal file
4
ansible/roles/linstor_repo/handlers/main.yml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
- name: Update APT cache
|
||||||
|
ansible.builtin.apt:
|
||||||
|
update_cache: true
|
||||||
@@ -1,24 +1,35 @@
|
|||||||
---
|
---
|
||||||
# tasks file for linstor_repo
|
# tasks file for linstor_repo
|
||||||
|
# Target: Proxmox 9 (Debian 13 / Trixie) with DRBD 9
|
||||||
|
|
||||||
|
# linbit_proxmox_version defaults to: Debian major version minus 4
|
||||||
|
# (Debian 13 = PVE 9, Debian 12 = PVE 8, etc.)
|
||||||
|
# Override this variable in group_vars if LINBIT releases a new repo name.
|
||||||
|
|
||||||
- name: Download LINBIT keyring package
|
- name: Download LINBIT keyring package
|
||||||
ansible.builtin.get_url:
|
ansible.builtin.get_url:
|
||||||
url: https://packages.linbit.com/public/linbit-keyring.deb
|
url: https://packages.linbit.com/public/linbit-keyring.deb
|
||||||
dest: /tmp/linbit-keyring.deb
|
dest: /tmp/linbit-keyring.deb
|
||||||
mode: '0644'
|
mode: '0644'
|
||||||
|
check_mode: false
|
||||||
|
|
||||||
- name: Install LINBIT keyring
|
- name: Install LINBIT keyring
|
||||||
ansible.builtin.apt:
|
ansible.builtin.apt:
|
||||||
deb: /tmp/linbit-keyring.deb
|
deb: /tmp/linbit-keyring.deb
|
||||||
state: present
|
state: present
|
||||||
|
|
||||||
- name: Add LINBIT APT repository for Proxmox
|
- name: Remove legacy LINBIT .list file (old APT format, prevents duplicates)
|
||||||
ansible.builtin.apt_repository:
|
ansible.builtin.file:
|
||||||
repo: >-
|
path: /etc/apt/sources.list.d/linbit.list
|
||||||
deb [signed-by=/etc/apt/trusted.gpg.d/linbit-keyring.gpg]
|
state: absent
|
||||||
https://packages.linbit.com/public/
|
|
||||||
proxmox-{{ ansible_facts['distribution_major_version'] | int - 4 }}
|
- name: Add LINBIT APT repository for Proxmox (DEB822 format)
|
||||||
drbd-9
|
ansible.builtin.deb822_repository:
|
||||||
filename: linbit
|
name: linbit
|
||||||
|
types: deb
|
||||||
|
uris: https://packages.linbit.com/public/
|
||||||
|
suites: "proxmox-{{ linbit_proxmox_version | default(ansible_facts['distribution_major_version'] | int - 4) }}"
|
||||||
|
components: drbd-9
|
||||||
|
signed_by: /etc/apt/trusted.gpg.d/linbit-keyring.gpg
|
||||||
state: present
|
state: present
|
||||||
update_cache: true
|
notify: Update APT cache
|
||||||
|
|||||||
@@ -1,22 +1,66 @@
|
|||||||
---
|
---
|
||||||
# tasks file for linstor_storage
|
# tasks file for linstor_storage
|
||||||
|
# NOTE: linstor_storage_pools[].targets keys are Ansible inventory_hostname values.
|
||||||
|
# All Linstor registrations use ansible_hostname (real system hostname) instead,
|
||||||
|
# to match Proxmox node names. The mapping is done per-host at registration time.
|
||||||
|
|
||||||
- name: Configure LVM global_filter for DRBD safely
|
- name: Configure LVM global_filter for DRBD safely
|
||||||
ansible.builtin.lineinfile:
|
ansible.builtin.lineinfile:
|
||||||
path: /etc/lvm/lvm.conf
|
path: /etc/lvm/lvm.conf
|
||||||
insertafter: '^[ \t]*devices[ \t]*\{'
|
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]*='
|
||||||
regexp: '^[ \t]*global_filter[ \t]*=.*r\|\^/dev/drbd\|'
|
|
||||||
line: ' global_filter = [ "r|^/dev/drbd|", "r|^/dev/mapper/[lL]instor|" ]'
|
line: ' global_filter = [ "r|^/dev/drbd|", "r|^/dev/mapper/[lL]instor|" ]'
|
||||||
state: present
|
state: present
|
||||||
|
|
||||||
|
- name: Remove duplicate global_filter entries from lvm.conf
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
count=$(grep -c '^[[:space:]]*global_filter[[:space:]]*=' /etc/lvm/lvm.conf)
|
||||||
|
if [ "$count" -gt 1 ]; then
|
||||||
|
awk '/^[[:space:]]*global_filter[[:space:]]*=/{if(++n>1)next}1' /etc/lvm/lvm.conf > /tmp/lvm.conf.dedup
|
||||||
|
mv /tmp/lvm.conf.dedup /etc/lvm/lvm.conf
|
||||||
|
echo "changed: removed $((count - 1)) duplicate(s)"
|
||||||
|
fi
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
register: lvm_dedup_result
|
||||||
|
changed_when: "'changed' in lvm_dedup_result.stdout"
|
||||||
|
|
||||||
|
- name: Wipe existing signatures and partition tables from raw disks
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
DEV="{{ item.targets[inventory_hostname] }}"
|
||||||
|
VG="{{ item.vg_name }}"
|
||||||
|
|
||||||
|
# Deactivate VG properly if LVM still knows about it
|
||||||
|
vgchange -an "$VG" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Forcibly remove any stale dm-mapper devices for this VG (handles the case
|
||||||
|
# where the VG metadata was already wiped but kernel devices are still active)
|
||||||
|
dmsetup ls 2>/dev/null \
|
||||||
|
| awk -v vg="$VG" 'index($1, vg"-") == 1 { print $1 }' \
|
||||||
|
| xargs -r dmsetup remove --force 2>/dev/null || true
|
||||||
|
|
||||||
|
wipefs -a "$DEV"
|
||||||
|
sgdisk --zap-all "$DEV"
|
||||||
|
partprobe "$DEV" 2>/dev/null || true
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
changed_when: true
|
||||||
|
loop: "{{ linstor_storage_pools }}"
|
||||||
|
when:
|
||||||
|
- wipe_linstor_disks | default(false) | bool
|
||||||
|
- item.targets[inventory_hostname] is defined
|
||||||
|
- item.targets[inventory_hostname].startswith('/dev/')
|
||||||
|
|
||||||
- name: Ensure Volume Groups exist for raw disks
|
- name: Ensure Volume Groups exist for raw disks
|
||||||
community.general.lvg:
|
community.general.lvg:
|
||||||
vg: "{{ item.vg_name }}"
|
vg: "{{ item.vg_name }}"
|
||||||
pvs: "{{ item.targets[inventory_hostname] }}"
|
pvs: "{{ item.targets[inventory_hostname] }}"
|
||||||
|
pv_options: "{{ '--force --force' if wipe_linstor_disks | default(false) | bool else '--force' }}"
|
||||||
loop: "{{ linstor_storage_pools }}"
|
loop: "{{ linstor_storage_pools }}"
|
||||||
# Jinja2 magic: Only run this LVM task if the target string starts with '/dev/'
|
# Only run for pools that target this node AND use a raw block device
|
||||||
when: item.targets[inventory_hostname].startswith('/dev/')
|
when:
|
||||||
|
- item.targets[inventory_hostname] is defined
|
||||||
|
- item.targets[inventory_hostname].startswith('/dev/')
|
||||||
|
|
||||||
- name: Ensure LVM Thin Pools exist for raw disks
|
- name: Ensure LVM Thin Pools exist for raw disks
|
||||||
community.general.lvol:
|
community.general.lvol:
|
||||||
@@ -26,25 +70,31 @@
|
|||||||
opts: "-T -Zn"
|
opts: "-T -Zn"
|
||||||
shrink: false
|
shrink: false
|
||||||
loop: "{{ linstor_storage_pools }}"
|
loop: "{{ linstor_storage_pools }}"
|
||||||
when: item.targets[inventory_hostname].startswith('/dev/')
|
when:
|
||||||
|
- item.targets[inventory_hostname] is defined
|
||||||
|
- item.targets[inventory_hostname].startswith('/dev/')
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# LINSTOR STORAGE REGISTRATION (Executed ONLY on Node 1)
|
# LINSTOR STORAGE REGISTRATION (Executed ONLY on Node 1)
|
||||||
|
# Uses ansible_hostname (real system hostname) for Linstor node names
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
- name: Register Storage Pools in LINSTOR
|
- name: Register Storage Pools in LINSTOR
|
||||||
ansible.builtin.shell: |
|
ansible.builtin.shell: |
|
||||||
set -o pipefail
|
set -o pipefail
|
||||||
|
|
||||||
# We use Jinja2 to write a custom bash loop for exactly the nodes in our inventory
|
# Iterate over all inventory hosts for this pool.
|
||||||
|
# Use ansible_hostname (real system hostname) as Linstor node name.
|
||||||
{% for host in groups['proxmox'] %}
|
{% for host in groups['proxmox'] %}
|
||||||
|
{% if item.targets[host] is defined %}
|
||||||
{% set target = item.targets[host] %}
|
{% set target = item.targets[host] %}
|
||||||
{% set lvm_path = (item.vg_name + '/' + item.thin_name) if target.startswith('/dev/') else target %}
|
{% set lvm_path = (item.vg_name + '/' + item.thin_name) if target.startswith('/dev/') else target %}
|
||||||
|
NODE_NAME="{{ hostvars[host]['ansible_facts']['hostname'] }}"
|
||||||
if ! linstor storage-pool list | grep -q " {{ host }} .* {{ item.pool_name }} "; then
|
if ! linstor storage-pool list | grep -q " ${NODE_NAME} .* {{ item.pool_name }} "; then
|
||||||
linstor storage-pool create lvmthin "{{ host }}" "{{ item.pool_name }}" "{{ lvm_path }}" > /dev/null
|
linstor storage-pool create lvmthin "${NODE_NAME}" "{{ item.pool_name }}" "{{ lvm_path }}" > /dev/null
|
||||||
echo "changed"
|
echo "changed"
|
||||||
fi
|
fi
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
args:
|
args:
|
||||||
executable: /bin/bash
|
executable: /bin/bash
|
||||||
@@ -57,7 +107,9 @@
|
|||||||
ansible.builtin.shell: |
|
ansible.builtin.shell: |
|
||||||
set -o pipefail
|
set -o pipefail
|
||||||
if ! linstor resource-group list | grep -q " {{ item.rg_name }} "; then
|
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 resource-group create "{{ item.rg_name }}" \
|
||||||
|
--storage-pool "{{ item.pool_name }}" \
|
||||||
|
--place-count {{ groups['proxmox'] | length }} > /dev/null
|
||||||
linstor volume-group create "{{ item.rg_name }}" > /dev/null
|
linstor volume-group create "{{ item.rg_name }}" > /dev/null
|
||||||
echo "changed"
|
echo "changed"
|
||||||
fi
|
fi
|
||||||
|
|||||||
1
ansible/roles/p2p_network/handlers/main.yml
Normal file
1
ansible/roles/p2p_network/handlers/main.yml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
---
|
||||||
28
ansible/roles/p2p_network/tasks/main.yml
Normal file
28
ansible/roles/p2p_network/tasks/main.yml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
- name: Validate p2p NICs exist on this node
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- priv_nic_1 in ansible_facts['interfaces']
|
||||||
|
- priv_nic_2 in ansible_facts['interfaces']
|
||||||
|
fail_msg: >-
|
||||||
|
One or both p2p NICs not found on {{ inventory_hostname }}.
|
||||||
|
Expected: {{ priv_nic_1 }}, {{ priv_nic_2 }}.
|
||||||
|
Available interfaces: {{ ansible_facts['interfaces'] | join(', ') }}
|
||||||
|
|
||||||
|
- name: Configure p2p bridge in /etc/network/interfaces
|
||||||
|
ansible.builtin.blockinfile:
|
||||||
|
path: /etc/network/interfaces
|
||||||
|
marker: "# {mark} ANSIBLE MANAGED BLOCK - p2p bridge"
|
||||||
|
block: |
|
||||||
|
auto p2p
|
||||||
|
iface p2p inet static
|
||||||
|
address {{ priv_ip }}/{{ ha_vip_cidr }}
|
||||||
|
bridge-ports {{ priv_nic_1 }} {{ priv_nic_2 }}
|
||||||
|
bridge-stp on
|
||||||
|
bridge-fd 0
|
||||||
|
register: p2p_network_p2p_bridge_config
|
||||||
|
|
||||||
|
- name: Apply network config # noqa: no-handler
|
||||||
|
ansible.builtin.command: ifreload -a
|
||||||
|
changed_when: true
|
||||||
|
when: p2p_network_p2p_bridge_config is changed
|
||||||
Reference in New Issue
Block a user