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
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
646f2bbc8f
commit
7031d7dbb3
@@ -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 }}
|
||||||
Reference in New Issue
Block a user