--- # Дрейф между реестром homelab_services и фактическим состоянием Proxmox. # # ЗАЧЕМ # Реестр — источник правды, но программно его потребляют лишь несколько мест. # VMID, адрес и ресурсы легко разъезжаются с реальностью, и заметить это # раньше было нечем: `make status` показывает состояние, но не сверяет его # с задекларированным, и его exit code намеренно не является gate'ом. # # ОТЛИЧИЕ ОТ status.yml # Этот плейбук — именно gate: при любом расхождении он завершается ошибкой. # Годится для cron и CI. Строго read-only: только `pct config`. # # make validate # make validate EXTRA="--limit cloud-pc" - name: Read actual LXC configuration from the Proxmox nodes hosts: pve_nodes gather_facts: false tasks: - name: Read pct config for the services declared on this node ansible.builtin.command: "pct config {{ item.value.vmid }}" loop: >- {{ homelab_services | dict2items | selectattr('value.node', 'eq', inventory_hostname) | list }} loop_control: label: "{{ item.key }} ({{ item.value.vmid }})" register: validate_pct changed_when: false failed_when: false check_mode: false # Сравниваются только однозначные поля. rootfs намеренно пропущен: в реестре # он записан как "data:32", а pct отдаёт "data:vm-141-disk-0,size=32G" — # это разные представления, и их сверка требует отдельного парсера. - name: Collect drift for this node ansible.builtin.set_fact: validate_drift: "{{ validate_drift | default([]) + item_drift }}" loop: "{{ validate_pct.results }}" loop_control: label: "{{ item.item.key }}" vars: svc: "{{ item.item.value }}" sname: "{{ item.item.key }}" gone: "{{ item.rc | default(1) != 0 }}" out: "{{ item.stdout | default('') }}" a_host: "{{ out | regex_search('(?m)^hostname: (\\S+)', '\\1') | default([''], true) | first }}" a_cores: "{{ out | regex_search('(?m)^cores: (\\d+)', '\\1') | default([''], true) | first }}" a_mem: "{{ out | regex_search('(?m)^memory: (\\d+)', '\\1') | default([''], true) | first }}" a_swap: "{{ out | regex_search('(?m)^swap: (\\d+)', '\\1') | default([''], true) | first }}" a_ip: "{{ out | regex_search('ip=([0-9.]+)', '\\1') | default([''], true) | first }}" item_drift: >- {{ ([sname ~ ': VMID ' ~ svc.vmid ~ ' отсутствует на узле ' ~ inventory_hostname] if gone else []) + ([sname ~ ': hostname=' ~ a_host ~ ', в реестре ' ~ svc.hostname] if (not gone and a_host != svc.hostname) else []) + ([sname ~ ': ip=' ~ a_ip ~ ', в реестре ' ~ svc.ip] if (not gone and a_ip != svc.ip) else []) + ([sname ~ ': cores=' ~ a_cores ~ ', в реестре ' ~ svc.lxc.cores] if (not gone and svc.lxc.cores is not none and a_cores | string != svc.lxc.cores | string) else []) + ([sname ~ ': memory=' ~ a_mem ~ ', в реестре ' ~ svc.lxc.memory] if (not gone and svc.lxc.memory is not none and a_mem | string != svc.lxc.memory | string) else []) + ([sname ~ ': swap=' ~ a_swap ~ ', в реестре ' ~ svc.lxc.swap] if (not gone and svc.lxc.swap is not none and a_swap | string != svc.lxc.swap | string) else []) }} - name: Report registry drift hosts: pve_nodes gather_facts: false run_once: true tasks: - name: Fail when the registry disagrees with Proxmox ansible.builtin.assert: that: - all_drift | length == 0 success_msg: >- Реестр совпадает с Proxmox: проверено сервисов — {{ homelab_services | dict2items | selectattr('value.node', 'in', groups['pve_nodes']) | list | length }}. fail_msg: >- {{ ['Реестр разошёлся с Proxmox:'] + (all_drift | map('regex_replace', '^', ' - ') | list) }} vars: all_drift: >- {{ groups['pve_nodes'] | map('extract', hostvars, 'validate_drift') | select('defined') | flatten }}