diff --git a/ansible/playbooks/validate.yml b/ansible/playbooks/validate.yml new file mode 100644 index 0000000..9a863ef --- /dev/null +++ b/ansible/playbooks/validate.yml @@ -0,0 +1,85 @@ +--- +# Дрейф между реестром 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 }}