Bug: selectattr 'match' in ludus.yml causes VM config collision when one vm_name is a prefix of another

Bug Description

In ludus.yml, all selectattr('vm_name', 'match', inventory_hostname) calls use Jinja2's match test, which performs a regex prefix match, not an exact string comparison. This causes a silent configuration collision when one VM's name is a prefix of another's.

For example, with VMs named DF-windows and DF-windows-jump, the expression:

ludus | selectattr('vm_name', 'match', 'DF-windows') | first

matches both DF-windows and DF-windows-jump (since "DF-windows-jump" starts with "DF-windows"). Whichever appears first in the range config list wins, silently returning the wrong VM's configuration.

Impact

This affects all variable lookups in ludus.yml that use this pattern (~56 occurrences), including:

  • static_ip — the wrong static IP is assigned to the VM
  • vlan — wrong VLAN used for IP construction
  • default_gateway — wrong gateway
  • vm_hostname — wrong hostname
  • dns_server — wrong DNS server
  • custom_roles_list — wrong roles
  • role_vars — wrong role variables
  • All domain, autologon, chocolatey, and office variable lookups

When the collision occurs during the "Configure IP and Hostname" phase, the affected VM gets the wrong VM's static IP. Subsequent tasks (DNS, sysprep, hostname, user roles) then connect to the wrong host, compounding the misconfiguration. The Proxmox qemu-guest-agent confirms the wrong IP is genuinely set on the VM's network interface.

Reproduction

  1. Create a range config with two VMs where one name is a prefix of the other:
    ludus:
      - vm_name: "{{ range_id }}-windows-jump"
        vlan: 20
        ip_last_octet: 221
        # ...
      - vm_name: "{{ range_id }}-windows"
        vlan: 22
        ip_last_octet: 60
        # ...
  2. Run ludus range deploy
  3. Observe that the second VM (-windows) gets the first VM's IP (10.x.20.221) instead of its own (10.x.22.60)

The bug is order-dependent: placing the shorter-named VM first in the config may mask the issue since | first would then return the correct entry. Placing the longer-named VM first triggers it.

Root Cause

Jinja2's match test (Ansible docs) performs a regex search anchored at the start of the string. It does NOT require a full match. So 'DF-windows-jump' is match('DF-windows') evaluates to True.

All 56 occurrences of selectattr('vm_name', 'match', inventory_hostname) in ludus.yml are affected.

proxmox.py is NOT affected — it correctly uses == for exact comparison in check_ip_addresses().

Fix

Replace 'match' with 'equalto' in all selectattr calls that compare vm_name to inventory_hostname:

- vlan: \"{{ (ludus | selectattr('vm_name', 'match', inventory_hostname) | first).vlan }}\"
+ vlan: \"{{ (ludus | selectattr('vm_name', 'equalto', inventory_hostname) | first).vlan }}\"

This should be applied to all ~56 instances in ludus.yml. The 4 instances that use match on domain.fqdn and domain.role fields are unaffected and can remain as-is.

Environment

Debug Evidence

Added instrumentation to proxmox.py's check_ip_addresses() and qemu_agent_info(). The debug output confirmed:

  1. First inventory run: Proxmox agent for VMID 112 (DF-windows) correctly reports DHCP IP 10.2.22.206 on VLAN 22
  2. Configure IP phase runsludus.yml resolves static_ip for DF-windows using selectattr('vm_name', 'match', 'DF-windows') | first, which returns DF-windows-jump's config (vlan=20, octet=221), setting static IP to 10.2.20.221
  3. Second inventory run: Agent for VMID 112 now reports 10.2.20.221 — confirming the wrong IP was actually applied to the VM's network interface