Files
ansible/playbooks/discover_guests.yml
T

216 lines
9.7 KiB
YAML

---
# 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