Files
proxmox-post-install/ansible/deploy.yml
2026-04-24 12:58:56 +00:00

254 lines
9.4 KiB
YAML

---
- name: Converged Proxmox & Optional Linstor Deployment
hosts: proxmox
become: true
gather_facts: true
any_errors_fatal: true
vars:
# Dynamically detect if we are in Single-Node or Cluster mode
is_single_node: "{{ groups['proxmox'] | length == 1 }}"
# standalone_mode can be set manually in group_vars to bypass cluster/linstor tasks
# 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) }}"
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 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)
# ==========================================
- 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
ansible.builtin.command: pvesubscription set -k {{ pve_enterprise_key }}
when: pve_enterprise_key | default('') | length > 0
register: pve_key_result
changed_when: "'successfully' in pve_key_result.stdout"
- name: Ensure Legacy .list Files are Removed (Prevent APT Duplicates)
ansible.builtin.file:
path: "{{ item }}"
state: absent
loop:
- /etc/apt/sources.list.d/pve-enterprise.list
- /etc/apt/sources.list.d/pve-no-subscription.list
- /etc/apt/sources.list.d/ceph.list
- name: Manage Proxmox Enterprise Repository (DEB822)
ansible.builtin.deb822_repository:
name: pve-enterprise
types: deb
uris: https://enterprise.proxmox.com/debian/pve
suites: trixie
components: pve-enterprise
signed_by: /usr/share/keyrings/proxmox-archive-keyring.gpg
state: "{{ 'present' if pve_enterprise_key | default('') | length > 0 else 'absent' }}"
- name: Manage Proxmox No-Subscription Repository (DEB822)
ansible.builtin.deb822_repository:
name: pve-no-subscription
types: deb
uris: http://download.proxmox.com/debian/pve
suites: trixie
components: pve-no-subscription
signed_by: /usr/share/keyrings/proxmox-archive-keyring.gpg
state: "{{ 'absent' if pve_enterprise_key | default('') | length > 0 else 'present' }}"
- name: Remove Ceph Enterprise Repository (Unused in Linstor setups)
ansible.builtin.file:
path: /etc/apt/sources.list.d/ceph.sources
state: absent
when: pve_enterprise_key | default('') | length == 0
- name: Add Linstor Repositories
ansible.builtin.include_role:
name: linstor_repo
when:
- deploy_linstor | bool
- not _skip_cluster | bool
# ==========================================
# 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
ansible.builtin.apt:
update_cache: true
upgrade: dist
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)
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
- not _skip_cluster | bool
- name: Check if a reboot is required by APT
ansible.builtin.stat:
path: /var/run/reboot-required
register: reboot_required_file
- name: Reboot the node gracefully
ansible.builtin.reboot:
msg: "Rebooting node to apply new kernel/system updates"
reboot_timeout: 600
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
# ==========================================
- name: PROXMOX CLUSTERING (Skipped in Single Node / Standalone Mode)
when: not _skip_cluster | bool
block:
- name: Ensure SSH keypair exists for root
ansible.builtin.user:
name: root
generate_ssh_key: true
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: 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
ansible.builtin.blockinfile:
path: /root/.ssh/config
create: true
mode: '0600'
block: |
Host {{ _corosync_subnet }} {{ _mgmt_subnet }}
StrictHostKeyChecking no
UserKnownHostsFile=/dev/null
- name: Check current Proxmox cluster status
ansible.builtin.command: pvecm status
register: pvecm_status
ignore_errors: true
changed_when: false
- name: Initialize the Proxmox Cluster (Master Node)
ansible.builtin.command: pvecm create {{ pve_cluster_name }} --link0 {{ corosync_ip }}
changed_when: true
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 — one at a time)
ansible.builtin.command: >
pvecm add {{ master_corosync_ip }} --link0 {{ corosync_ip }} --use_ssh yes
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
- not is_master | bool
throttle: 1 # Prevent race condition: pvecm add does not support parallel joins
async: 120
poll: 10
# ==========================================
# PHASE 4: LINSTOR CONFIGURATION
# ==========================================
- name: LINSTOR CONFIGURATION (Skipped if deploy_linstor=false or Single Node / Standalone)
when:
- deploy_linstor | bool
- not _skip_cluster | 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