--- # Helper tasks file for playbooks/app/provision_vms.yml # Provisions a single (project, env) guest and adds it to dynamic inventory. - name: Set environment facts ansible.builtin.set_fact: env_name: "{{ env_item.key }}" env_def: "{{ env_item.value }}" guest_name: "{{ env_item.value.name | default(project_key ~ '-' ~ env_item.key) }}" # vmid is optional; if omitted, we will manage idempotency by unique guest_name guest_vmid: "{{ env_item.value.vmid | default(none) }}" - name: Normalize recreate_existing_envs to a list ansible.builtin.set_fact: recreate_envs_list: >- {{ (recreate_existing_envs.split(',') | map('trim') | list) if (recreate_existing_envs is defined and recreate_existing_envs is string) else (recreate_existing_envs | default([])) }} - name: Check if Proxmox guest already exists (by VMID when provided) community.proxmox.proxmox_vm_info: api_host: "{{ proxmox_host }}" api_port: "{{ proxmox_api_port | default(8006) }}" validate_certs: "{{ proxmox_validate_certs | default(false) }}" api_user: "{{ proxmox_user }}" api_password: "{{ vault_proxmox_password | default(omit) }}" api_token_id: "{{ proxmox_token_id | default(omit, true) }}" api_token_secret: "{{ vault_proxmox_token | default(omit, true) }}" node: "{{ proxmox_node }}" type: lxc vmid: "{{ guest_vmid }}" register: proxmox_guest_info_vmid when: guest_vmid is not none - name: Check if Proxmox guest already exists (by name when VMID omitted) community.proxmox.proxmox_vm_info: api_host: "{{ proxmox_host }}" api_port: "{{ proxmox_api_port | default(8006) }}" validate_certs: "{{ proxmox_validate_certs | default(false) }}" api_user: "{{ proxmox_user }}" api_password: "{{ vault_proxmox_password | default(omit) }}" api_token_id: "{{ proxmox_token_id | default(omit, true) }}" api_token_secret: "{{ vault_proxmox_token | default(omit, true) }}" node: "{{ proxmox_node }}" type: lxc name: "{{ guest_name }}" register: proxmox_guest_info_name when: guest_vmid is none - name: Set guest_exists fact ansible.builtin.set_fact: guest_exists: >- {{ ((proxmox_guest_info_vmid.proxmox_vms | default([])) | length > 0) if (guest_vmid is not none) else ((proxmox_guest_info_name.proxmox_vms | default([])) | length > 0) }} - name: "Guardrail: abort if VMID exists but name does not match (prevents overwriting other guests)" ansible.builtin.fail: msg: >- Refusing to use VMID {{ guest_vmid }} for {{ guest_name }} because it already exists as "{{ (proxmox_guest_info_vmid.proxmox_vms[0].name | default('UNKNOWN')) }}". Pick a different vmid range in app_projects or omit vmid to auto-allocate. when: - guest_vmid is not none - (proxmox_guest_info_vmid.proxmox_vms | default([])) | length > 0 - (proxmox_guest_info_vmid.proxmox_vms[0].name | default('')) != guest_name - not (allow_vmid_collision | default(false) | bool) - name: Delete existing guest if requested (recreate) community.proxmox.proxmox: api_host: "{{ proxmox_host }}" api_port: "{{ proxmox_api_port | default(8006) }}" validate_certs: "{{ proxmox_validate_certs | default(false) }}" api_user: "{{ proxmox_user }}" api_password: "{{ vault_proxmox_password | default(omit) }}" api_token_id: "{{ proxmox_token_id | default(omit, true) }}" api_token_secret: "{{ vault_proxmox_token | default(omit, true) }}" node: "{{ proxmox_node }}" vmid: "{{ guest_vmid }}" purge: true force: true state: absent when: - guest_exists | bool - guest_vmid is not none - recreate_existing_guests | default(false) | bool or (env_name in recreate_envs_list) - name: Mark guest as not existing after delete ansible.builtin.set_fact: guest_exists: false when: - guest_vmid is not none - recreate_existing_guests | default(false) | bool or (env_name in recreate_envs_list) - name: "Preflight: detect IP conflicts on Proxmox (existing LXC net0 ip=)" community.proxmox.proxmox_vm_info: api_host: "{{ proxmox_host }}" api_port: "{{ proxmox_api_port | default(8006) }}" validate_certs: "{{ proxmox_validate_certs | default(false) }}" api_user: "{{ proxmox_user }}" api_password: "{{ vault_proxmox_password | default(omit) }}" api_token_id: "{{ proxmox_token_id | default(omit, true) }}" api_token_secret: "{{ vault_proxmox_token | default(omit, true) }}" node: "{{ proxmox_node }}" type: lxc config: current register: proxmox_all_lxc when: - (env_def.ip | default('')) | length > 0 - not (allow_ip_conflicts | default(false) | bool) - not (guest_exists | default(false) | bool) - name: Set proxmox_ip_conflicts fact ansible.builtin.set_fact: proxmox_ip_conflicts: >- {%- set conflicts = [] -%} {%- set target_ip = ((env_def.ip | string).split('/')[0]) -%} {%- for vm in (proxmox_all_lxc.proxmox_vms | default([])) -%} {%- set cfg_net0 = ( vm['config']['net0'] if ( vm is mapping and ('config' in vm) and (vm['config'] is mapping) and ('net0' in vm['config']) ) else none ) -%} {%- set vm_netif = (vm['netif'] if (vm is mapping and ('netif' in vm)) else none) -%} {%- set net0 = ( cfg_net0 if (cfg_net0 is not none) else ( vm_netif['net0'] if (vm_netif is mapping and ('net0' in vm_netif)) else ( vm_netif if (vm_netif is string) else (vm['net0'] if (vm is mapping and ('net0' in vm)) else '') ) ) ) | string -%} {%- set vm_ip = (net0 | regex_search('(?:^|,)ip=([^,]+)', '\\1') | default('')) | regex_replace('/.*$', '') -%} {%- if (vm_ip | length) > 0 and vm_ip == target_ip -%} {%- set _ = conflicts.append({'vmid': (vm.vmid | default('') | string), 'name': (vm.name | default('') | string), 'net0': net0}) -%} {%- endif -%} {%- endfor -%} {{ conflicts }} when: - proxmox_all_lxc is defined - (env_def.ip | default('')) | length > 0 - not (allow_ip_conflicts | default(false) | bool) - not (guest_exists | default(false) | bool) - name: Abort if IP is already assigned to an existing Proxmox LXC ansible.builtin.fail: msg: >- Refusing to provision {{ guest_name }} because IP {{ (env_def.ip | string).split('/')[0] }} is already present in Proxmox LXC net0 config: {{ proxmox_ip_conflicts }}. Fix app_projects IPs or set -e allow_ip_conflicts=true. when: - (env_def.ip | default('')) | length > 0 - not (allow_ip_conflicts | default(false) | bool) - not (guest_exists | default(false) | bool) - (proxmox_ip_conflicts | default([])) | length > 0 - name: "Preflight: fail if target IP responds (avoid accidental duplicate IP)" ansible.builtin.command: "ping -c 1 -W 1 {{ (env_def.ip | string).split('/')[0] }}" register: ip_ping changed_when: false failed_when: false when: - (env_def.ip | default('')) | length > 0 - not (allow_ip_conflicts | default(false) | bool) - not (guest_exists | default(false) | bool) - name: Abort if IP appears to be in use ansible.builtin.fail: msg: >- Refusing to provision {{ guest_name }} because IP {{ (env_def.ip | string).split('/')[0] }} responded to ping. Fix app_projects IPs or set -e allow_ip_conflicts=true. Note: this guardrail is ping-based; if your network blocks ICMP, an in-use IP may not respond. when: - (env_def.ip | default('')) | length > 0 - not (allow_ip_conflicts | default(false) | bool) - not (guest_exists | default(false) | bool) - ip_ping.rc == 0 - name: Provision LXC guest for project/env ansible.builtin.include_role: name: proxmox_vm vars: # NOTE: Use hostvars['localhost'] for defaults to avoid recursive self-references proxmox_guest_type: "{{ project_def.guest_defaults.guest_type | default(hostvars['localhost'].proxmox_guest_type | default('lxc')) }}" # Only pass vmid when provided; otherwise Proxmox will auto-allocate lxc_vmid: "{{ guest_vmid if guest_vmid is not none else omit }}" lxc_hostname: "{{ guest_name }}" lxc_ostemplate: "{{ project_def.lxc_ostemplate | default(hostvars['localhost'].lxc_ostemplate) }}" lxc_storage: "{{ project_def.lxc_storage | default(hostvars['localhost'].lxc_storage) }}" lxc_network_bridge: "{{ project_def.lxc_network_bridge | default(hostvars['localhost'].lxc_network_bridge) }}" lxc_unprivileged: "{{ project_def.lxc_unprivileged | default(hostvars['localhost'].lxc_unprivileged) }}" lxc_features_list: "{{ project_def.lxc_features_list | default(hostvars['localhost'].lxc_features_list) }}" lxc_cores: "{{ project_def.guest_defaults.cores | default(hostvars['localhost'].lxc_cores) }}" lxc_memory_mb: "{{ project_def.guest_defaults.memory_mb | default(hostvars['localhost'].lxc_memory_mb) }}" lxc_swap_mb: "{{ project_def.guest_defaults.swap_mb | default(hostvars['localhost'].lxc_swap_mb) }}" lxc_rootfs_size_gb: "{{ project_def.guest_defaults.rootfs_size_gb | default(hostvars['localhost'].lxc_rootfs_size_gb) }}" lxc_ip: "{{ env_def.ip }}" lxc_gateway: "{{ env_def.gateway }}" lxc_nameserver: "{{ project_def.lxc_nameserver | default(hostvars['localhost'].lxc_nameserver) }}" lxc_pubkey: "{{ appuser_ssh_public_key | default('') }}" lxc_start_after_create: "{{ project_def.lxc_start_after_create | default(hostvars['localhost'].lxc_start_after_create) }}" - name: Wait for SSH to become available ansible.builtin.wait_for: host: "{{ (env_def.ip | string).split('/')[0] }}" port: 22 timeout: 300 when: (env_def.ip | default('')) | length > 0 - name: Add guest to dynamic inventory ansible.builtin.add_host: name: "{{ guest_name }}" groups: - "app_all" - "app_{{ project_key }}_all" - "app_{{ project_key }}_{{ env_name }}" ansible_host: "{{ (env_def.ip | string).split('/')[0] }}" ansible_user: root app_project: "{{ project_key }}" app_env: "{{ env_name }}"