Files
DmitryandClaude Sonnet 5 7031d7dbb3 feat: read-only service-registry to Proxmox drift gate
playbooks/validate.yml (make validate): reads pct config for every service in
homelab_services and fails if hostname, IP, cores, memory or swap disagree
with the registry. Read-only; meant as a pre/post gate around any inventory or
provisioning change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uoq5AVK8mkBgg83Mq6o5V
2026-09-03 07:04:29 +03:00

86 lines
4.6 KiB
YAML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
# Дрейф между реестром 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 }}