--- - 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 }}" # Existing clustering variables 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 (PVE 9 / Trixie) # ========================================== - 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) ansible.builtin.file: path: /etc/apt/sources.list.d/ceph.sources state: absent when: pve_enterprise_key | default('') | length == 0 - name: Add Linstor Repositories (Skipped in Standalone Node) ansible.builtin.include_role: name: linstor_repo when: - deploy_linstor | bool - not is_standalone | bool # ========================================== # PHASE 2: UPDATES & NAG REMOVAL # ========================================== - name: Update APT cache and run dist-upgrade ansible.builtin.apt: update_cache: yes upgrade: dist autoremove: yes - 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 (Skipped in Single Node) ansible.builtin.include_role: name: linstor_install when: - deploy_linstor | bool - not is_single_node | 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 # Wait up to 10 minutes for the server to return when: reboot_required_file.stat.exists # ========================================== # PHASE 3: PROXMOX CLUSTERING # ========================================== - name: PROXMOX CLUSTERING (Skipped in Single Node Mode) when: not is_single_node | bool block: - 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: LINSTOR CONFIGURATION (Skipped if deploy_linstor=false or Single Node) when: - deploy_linstor | bool - not is_single_node | 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