--- # Lists every container and VM on the Proxmox host and reports which ones are # missing from inventories/all.yml. A guest counts as managed when its LAN IP # matches an inventory ansible_host, or (if its IP is unknown, e.g. stopped) # when its name matches an inventory hostname. # # With adopt=true the Ansible SSH key is pushed into every unmanaged RUNNING # container straight from the host (pct exec), so a container created by hand # in the Proxmox UI becomes reachable without knowing its root password. # Afterwards: add the printed inventory snippet, then run bootstrap_lxc.yml. # # Extra vars (optional): # adopt - default false # lan_prefix - address prefix that counts as the LAN; used to pick the right address # when a guest has several (default 192.168., i.e. any lab subnet) # adopt_ssh_key - public key to push (default: the ansible-key from bootstrap_lxc.yml) - name: Discover Proxmox guests not in inventory hosts: proxmox become: true gather_facts: false vars: adopt: false lan_prefix: "192.168." lan_regex: "^{{ lan_prefix | regex_escape }}" adopt_ssh_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINegitHaQG8i+s412h3wTWf/KvpeX1CEpYFva9MhHR+7 ansible-key" # pct "ostype" -> inventory group group_map: centos: rocky ubuntu: ubuntu debian: ubuntu tasks: - name: Initialise working facts ansible.builtin.set_fact: guest_ips: {} guest_ostypes: {} guest_table: [] inventory_by_ip: {} - name: List all guests on the host ansible.builtin.command: pvesh get /cluster/resources --type vm --output-format json register: resources changed_when: false - name: Parse guest list ansible.builtin.set_fact: guests: "{{ resources.stdout | from_json | rejectattr('template', 'defined') | sort(attribute='vmid') | list + (resources.stdout | from_json | selectattr('template', 'defined') | selectattr('template', 'equalto', 0) | list) }}" - name: Read container OS type ansible.builtin.command: pct config {{ item.vmid }} loop: "{{ guests | selectattr('type', 'equalto', 'lxc') | list }}" loop_control: label: "{{ item.vmid }} {{ item.name | default('') }}" register: lxc_configs changed_when: false failed_when: false - name: Record container OS types ansible.builtin.set_fact: guest_ostypes: "{{ guest_ostypes | combine({(item.item.vmid | string): ostype}) }}" vars: ostype: "{{ (item.stdout | default('') | regex_search('(?m)^ostype: (\\S+)', '\\1')) | default(['-'], true) | first }}" loop: "{{ lxc_configs.results | default([]) }}" loop_control: label: "{{ item.item.vmid }}" - name: Read container IP addresses ansible.builtin.command: pct exec {{ item.vmid }} -- hostname -I loop: "{{ guests | selectattr('type', 'equalto', 'lxc') | selectattr('status', 'equalto', 'running') | list }}" loop_control: label: "{{ item.vmid }} {{ item.name | default('') }}" register: lxc_ips changed_when: false failed_when: false - name: Record container IP addresses ansible.builtin.set_fact: guest_ips: "{{ guest_ips | combine({(item.item.vmid | string): lan_ip}) }}" vars: all_ips: "{{ item.stdout | default('') | split }}" lan_ip: "{{ all_ips | select('match', lan_regex) | first | default(all_ips | first | default('-')) }}" loop: "{{ lxc_ips.results | default([]) }}" loop_control: label: "{{ item.item.vmid }}" - name: Read VM IP addresses via the guest agent ansible.builtin.command: qm guest cmd {{ item.vmid }} network-get-interfaces loop: "{{ guests | selectattr('type', 'equalto', 'qemu') | selectattr('status', 'equalto', 'running') | list }}" loop_control: label: "{{ item.vmid }} {{ item.name | default('') }}" register: qemu_ips changed_when: false failed_when: false - name: Record VM IP addresses ansible.builtin.set_fact: guest_ips: "{{ guest_ips | combine({(item.item.vmid | string): lan_ip}) }}" vars: agent_ok: "{{ item.rc == 0 and (item.stdout | default('') | trim) is match('^\\[') }}" all_ips: >- {{ ((item.stdout | from_json) if agent_ok else []) | map(attribute='ip-addresses', default=[]) | flatten | selectattr('ip-address-type', 'equalto', 'ipv4') | map(attribute='ip-address') | reject('match', '^127\\.') | list }} lan_ip: "{{ all_ips | select('match', lan_regex) | first | default(all_ips | first | default('-')) }}" loop: "{{ qemu_ips.results | default([]) }}" loop_control: label: "{{ item.item.vmid }}" - name: Index inventory hosts by address ansible.builtin.set_fact: inventory_by_ip: "{{ inventory_by_ip | combine({hostvars[item].ansible_host: item}) }}" loop: "{{ groups['all'] }}" when: hostvars[item].ansible_host is defined - name: Assemble guest table ansible.builtin.set_fact: guest_table: "{{ guest_table + [guest_entry] }}" vars: vmid: "{{ item.vmid | string }}" guest_name: "{{ item.name | default('-') }}" guest_ip: "{{ guest_ips[vmid] | default('-') }}" guest_ostype: "{{ guest_ostypes[vmid] | default('-') }}" inventory_match: "{{ inventory_by_ip[guest_ip] | default(guest_name if guest_name in groups['all'] else '') }}" guest_entry: vmid: "{{ vmid }}" type: "{{ item.type }}" status: "{{ item.status }}" name: "{{ guest_name }}" ip: "{{ guest_ip }}" group: "{{ (group_map[guest_ostype] | default(guest_ostype)) if item.type == 'lxc' else 'ubuntu (verify) + vms' }}" inventory_name: "{{ inventory_match }}" loop: "{{ guests }}" loop_control: label: "{{ item.vmid }} {{ item.name | default('') }}" - name: Collect unmanaged guests ansible.builtin.set_fact: unmanaged: "{{ guest_table | selectattr('inventory_name', 'equalto', '') | list }}" - name: Print guest report ansible.builtin.debug: msg: | ========================================================================================== PROXMOX GUESTS vs INVENTORY (host: {{ inventory_hostname }}) ========================================================================================== VMID TYPE STATUS NAME IP GROUP INVENTORY {% for g in guest_table %} {{ '%-5s %-5s %-8s %-18s %-16s %-9s %s' | format(g.vmid, g.type, g.status, g.name, g.ip, g.group.split(' ')[0], ('managed as ' ~ g.inventory_name) if g.inventory_name else 'NOT IN INVENTORY') }} {% endfor %} ------------------------------------------------------------------------------------------ TOTAL: {{ guest_table | length }} MANAGED: {{ (guest_table | length) - (unmanaged | length) }} UNMANAGED: {{ unmanaged | length }} ========================================================================================== - name: Warn on unmanaged guests ansible.builtin.debug: msg: "WARNING: {{ item.type }} {{ item.vmid }} ({{ item.name }}, {{ item.status }}, ip {{ item.ip }}) is not in inventories/all.yml" loop: "{{ unmanaged }}" loop_control: label: "{{ item.vmid }} {{ item.name }}" - name: Suggested inventory entries for unmanaged guests ansible.builtin.debug: msg: | Add to inventories/all.yml under the OS group shown, and list the name under lxc or vms: {% for g in unmanaged %} # {{ g.type }} {{ g.vmid }} -> group: {{ g.group }} {{ g.name }}: ansible_host: {{ g.ip if g.ip != '-' else lan_prefix ~ 'XXX # start the guest and rerun to discover its address' }} critical_services: [] {% endfor %} when: unmanaged | length > 0 - name: Push the Ansible SSH key into unmanaged running containers ansible.builtin.shell: >- pct exec {{ item.vmid }} -- sh -c 'mkdir -p /root/.ssh && chmod 700 /root/.ssh && touch /root/.ssh/authorized_keys && chmod 600 /root/.ssh/authorized_keys && if ! grep -qxF "{{ adopt_ssh_key }}" /root/.ssh/authorized_keys; then echo "{{ adopt_ssh_key }}" >> /root/.ssh/authorized_keys && echo KEY_ADDED; fi; command -v sshd >/dev/null 2>&1 && echo SSHD_OK || echo NO_SSHD' loop: "{{ unmanaged | selectattr('type', 'equalto', 'lxc') | selectattr('status', 'equalto', 'running') | list }}" loop_control: label: "{{ item.vmid }} {{ item.name }}" register: adopt_result changed_when: "'KEY_ADDED' in adopt_result.stdout" when: adopt | bool - name: Adoption summary ansible.builtin.debug: msg: >- {{ item.item.vmid }} ({{ item.item.name }}) - key {{ 'ADDED' if 'KEY_ADDED' in item.stdout else 'already present' }}, {{ 'sshd present' if 'SSHD_OK' in item.stdout else 'WARNING: no sshd in the container - install openssh-server before Ansible can reach it' }} - next: add it to inventories/all.yml and run bootstrap_lxc.yml loop: "{{ adopt_result.results | default([]) }}" loop_control: label: "{{ item.item.vmid }}" when: - adopt | bool - item.rc is defined - name: Note about unmanaged VMs ansible.builtin.debug: msg: "NOTE: {{ item.type }} {{ item.vmid }} ({{ item.name }}) is a VM; adopt only handles containers. Add its key via cloud-init or the console." loop: "{{ unmanaged | selectattr('type', 'equalto', 'qemu') | list }}" loop_control: label: "{{ item.vmid }} {{ item.name }}" when: adopt | bool