#!/usr/bin/env python3 """Le gabarit dore, face a ce que le SITE declare de lui. Ne corrige rien, regarde. POURQUOI CE CONTROLE EXISTE (2026-09-01). Le gabarit porte des choix qui NE SE CORRIGENT PAS APRES COUP — au premier rang, son type de machine. `q35` (PCIe) et non `i440fx` (PCI, le defaut de Proxmox). Convertir une machine deja installee d'un chipset a l'autre remplace son materiel sous un systeme qui croit connaitre le sien : les noms d'interfaces predictibles suivent le chemin PCI et changent, les chemins de disques bougent, et chaque panne qui s'ensuit ressemble a autre chose qu'a sa cause. C'est un constat paye en anomalies par l'exploitant, et c'est aussi pourquoi Set-OPS n'utilise pas l'image cloud officielle de Debian : elle est livree pour `i440fx`. UNE MACHINE NAIT `q35`, OU ELLE NE LE SERA JAMAIS PROPREMENT. CE QU'ON VERIFIE, ET POURQUOI ICI PLUTOT QU'AU CLONAGE. La propriete s'HERITE : si le gabarit est conforme, tous ses clones le sont. Verifier chaque copie coute a chaque creation et ne dit rien de plus que verifier la source une fois. On regarde donc la SOURCE. Ce n'est pas une preuve du harnais : elle exigerait le cluster, que `make prouver` ne suppose pas joignable. C'est un controle a la demande, comme `make genome-etat` et `make underlay-plan`. Usage : python3 scripts/gabarit_etat.py # code de sortie 0 si conforme """ from __future__ import annotations import subprocess import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent)) import underlay as underlay_mod # noqa: E402 # Ce que l'on compare EN `cle: valeur`. Le `stockage` se traite a part : il est # enfoui dans la ligne du disque, pas rendu comme une cle a lui. CHAMPS = ("machine", "bios") def _config_distante(noeud_nom: str, vmid: int) -> dict: """`qm config ` sur l'hyperviseur qui porte le gabarit. ON PASSE PAR LE SHELL DU NOEUD, pas par l'API : le poste de l'exploitant l'atteint deja (c'est ainsi qu'on lit les configurations de VM depuis toujours), et cela evite d'exiger un jeton la ou une lecture suffit. """ hote = _adresse_du_noeud(noeud_nom) if not hote: raise SystemExit(f"Noeud « {noeud_nom} » introuvable dans l'underlay de l'hebergeur.") r = subprocess.run( ["ssh", "-o", "ConnectTimeout=8", "-o", "BatchMode=yes", f"ansible@{hote}", f"sudo -n qm config {int(vmid)}"], capture_output=True, text=True, timeout=60) if r.returncode != 0: raise SystemExit(f"Lecture impossible sur {noeud_nom} ({hote}) : " f"{(r.stderr or '').strip()[:160]}") out = {} for ligne in r.stdout.splitlines(): if ":" in ligne: k, _, v = ligne.partition(":") out[k.strip()] = v.strip() return out def _adresse_du_noeud(nom: str) -> str: """L'adresse de gestion d'un hyperviseur, DERIVEE de l'underlay. Jamais ecrite ici : les trois noeuds ne sont pas consecutifs (`.41`, `.43`, `.47`), et sonder une adresse plausible est une erreur qu'aucun outil ne rattrape. """ u = underlay_mod.charger() for h in underlay_mod.hotes(u): if h.get("nom") == nom and h.get("role") == "hyperviseur" \ and str(h.get("ip", "")).startswith("192.168.11."): return str(h["ip"]) return "" def main() -> int: g = underlay_mod.gabarit() if not g: print("Le SITE ne declare aucun gabarit (cle `gabarit` de son plan). Rien a comparer.") return 0 vmid, noeud = g.get("vmid"), g.get("noeud") if not vmid or not noeud: print("Le gabarit declare est incomplet : il lui faut `vmid` et `noeud`.") return 1 cfg = _config_distante(str(noeud), int(vmid)) print(f"Gabarit {vmid} « {cfg.get('name', '?')} » sur {noeud}") ecarts = [] for champ in CHAMPS: attendu = str(g.get(champ, "")).strip() if not attendu: continue reel = cfg.get(champ, "") etat = "=" if reel == attendu else "≠" print(f" {champ:<10} declare {attendu:<12} reel {reel or '(absent)':<12} {etat}") if reel != attendu: ecarts.append(f"{champ} : declare « {attendu} », reel « {reel or 'absent'} »") # LE STOCKAGE NE SE LIT PAS EN `cle: valeur` — il est ENFOUI dans la ligne du disque : # # scsi0: CephNVMe:base-9006-disk-0,discard=on,iothread=1,size=16G,ssd=1 # ^^^^^^^^ # # On le compare quand meme, parce qu'il decide du temps de reproduction de tout # ecosysteme : mesure du 2026-09-10, 8 s par clone sur `CephNVMe` contre 352 a 480 s # sur `TrueNAS`. Un gabarit qui glisse en arriere ne casse rien — il rend simplement # chaque reconstruction quarante-cinq fois plus lente, sans qu'aucune erreur ne le dise. attendu_stockage = str(g.get("stockage", "")).strip() if attendu_stockage: disque = str(cfg.get("scsi0") or cfg.get("virtio0") or "") reel_stockage = disque.split(":", 1)[0].strip() if ":" in disque else "" etat = "=" if reel_stockage == attendu_stockage else "≠" print(f" {'stockage':<10} declare {attendu_stockage:<12} " f"reel {reel_stockage or '(absent)':<12} {etat}") if reel_stockage != attendu_stockage: ecarts.append(f"stockage : declare « {attendu_stockage} », reel " f"« {reel_stockage or 'absent'} » — chaque clone en paiera le prix") if not str(cfg.get("template", "")).strip() == "1": ecarts.append("ce VMID n'est PAS un gabarit (`template: 1` absent) — le cloner " "marcherait, mais il derivera au premier demarrage") if ecarts: print("\nECART — le gabarit ne porte pas ce que le SITE declare :") for e in ecarts: print(f" - {e}") # LE REMEDE DEPEND DE L'ECART, et ce n'est pas un detail de presentation : le # conseil « refabriquer le gabarit » etait imprime pour TOUT ecart, y compris un # simple deplacement de disque qui se repare en une commande sans rien refaire. # Un remede plus lourd que le mal se fait ignorer, puis le vrai avec lui. if any(e.startswith(("machine", "bios")) for e in ecarts): print("\n NE PAS CONVERTIR la machine : changer son chipset apres coup casse les") print(" noms d'interfaces et les chemins de disques. REFABRIQUER le gabarit.") if any(e.startswith("stockage") for e in ecarts): print(f"\n Le stockage, lui, se corrige SANS refabriquer :") print(f" qm move_disk {vmid} scsi0 {attendu_stockage}") print(" Sans `--delete`, l'ancien disque reste attache en `unused` — le") print(" retour arriere tient en une commande.") return 1 print("\nConforme — le gabarit porte ce que le SITE declare de lui.") return 0 if __name__ == "__main__": raise SystemExit(main())