diff --git a/CHANGELOG.md b/CHANGELOG.md index a2fdcb0..a86ef37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,46 @@ # CHANGELOG — Set-OPS +## 2026-09-01 — `q35` n'est pas un reglage : c'est la raison de la procedure manuelle + +**56 preuves.** L'exploitant a explique pourquoi Set-OPS n'utilise pas l'image cloud +officielle de Debian — et c'est un constat **paye en anomalies**, qui n'etait ecrit nulle +part. + +`genericcloud` est livree configuree pour **`i440fx`**, le defaut de Proxmox. La convertir +en `q35` apres coup ne change pas un parametre : ça **remplace le materiel virtuel sous un +systeme qui croit connaitre le sien**. + +``` +i440fx = PCI q35 = PCIe +la topologie des bus change, donc : + les noms d'interfaces predictibles suivent le chemin PCI et changent + les chemins de disques bougent + l'ordre d'enumeration des peripheriques n'est plus le meme +``` + +*La conversion n'est pas une correction — c'est une transplantation.* D'ou la regle : **une +machine nait `q35`, ou elle ne le sera jamais proprement**, et c'est l'installation depuis +l'ISO qui le garantit. + +Ces deux lignes n'existaient que comme une ligne de tableau dans la procedure. Elles +portent desormais leur **pourquoi**, et le SITE les declare comme **donnees** — plus +seulement comme prose. + +### `make gabarit-etat` + +Il compare le gabarit reel a ce que le site declare de lui. *Controle negatif verifie : +declarer `i440fx` le fait echouer.* + +**On verifie la SOURCE, pas chaque copie.** Ma premiere version gardait le clonage — mais +la propriete s'herite : verifier chaque clone coute a chaque creation sans rien dire de +plus que verifier le gabarit une fois. + +*Et trois fois j'ai devine la forme de la reponse au lieu de la regarder — `regex_search` a +groupe qui rend `None`, `proxmox_vm_info` sans `config: current` qui ne rend que l'etat. La +garde a declare « ? » sur une VM parfaitement conforme. **Une garde qui crie toujours est +pire qu'aucune** : on apprend a l'ignorer, et le jour ou elle a raison plus personne ne la +lit.* + ## 2026-09-01 — Le gabarit refabrique : minimal, et fabrique chez le SITE **56 preuves.** Le gabarit dore est refait — VMID **9006**, `modeleSetOPS-minimal`, quatre diff --git a/Makefile b/Makefile index 74b2591..9f003a5 100644 --- a/Makefile +++ b/Makefile @@ -943,6 +943,19 @@ site-appliquer: ## Applique un role aux machines du site — GROUPE= `q35` remplace +# le materiel sous un systeme qui croit connaitre le sien, et chaque panne qui s'ensuit +# ressemble a autre chose qu'a sa cause. On verifie donc la SOURCE — la propriete s'herite, +# tous les clones d'un gabarit conforme le sont. +# +# A LA DEMANDE, pas dans `make prouver` : ce controle exige le cluster, que le harnais ne +# suppose pas joignable. Meme nature que `genome-etat` et `underlay-plan`. +.PHONY: gabarit-etat +gabarit-etat: ## Le gabarit porte-t-il ce que le SITE declare ? — ne corrige rien, regarde + python3 scripts/gabarit_etat.py + .PHONY: genome-etat genome-etat: ## Ce que la forge du site porte, face au poste — ne corrige rien, regarde @set -e; \ diff --git a/docs/audit/preuve-2026-09-01.md b/docs/audit/preuve-2026-09-01.md index 8540aa7..7d19992 100644 --- a/docs/audit/preuve-2026-09-01.md +++ b/docs/audit/preuve-2026-09-01.md @@ -43,7 +43,7 @@ | P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 3 pool(s) Proxmox, 35 VM placee(s), aucun nom ni VMID en collision. | | P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 31 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 20, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv | | P30 | SDN EVPN : zones, VNets et sous-reseaux derives | AFF-112 | ✅ OK | CONFORME : SDN EVPN, 3 zone(s), 15 VNet(s), 15 sous-reseau(x), aucune collision. | -| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 53 scripts expliques et atteignables, 107 cibles make documentees, 63 roles avec README. | +| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 54 scripts expliques et atteignables, 108 cibles make documentees, 63 roles avec README. | | P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 41 exigence(s) de role, toutes satisfaites (128 cle(s) declaree(s) par l'instance). | | P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 33 revendication(s) de port, aucune collision entre roles co-localises (38 groupes). | | P34 | Chaque document declare son lecteur | — | ✅ OK | 42 document(s) declarent leur lecteur (29 genere(s) exempte(s)). | @@ -53,7 +53,7 @@ | P38 | Catalogue des services : la carte dit ce que le moteur fait | — | ✅ OK | Catalogue a jour : 38 role(s) serveur/client tous nommes, 39 groupe(s) cite(s) en table existent tous. | | P39 | Glossaire : tout mot employe est enseigne | — | ✅ OK | Glossaire complet : 81 terme(s) du jargon expliques, 15 lien(s) valides, 27 page(s) de wiki toutes atteignables. | | P40 | Parente : l'ecosysteme sait de quoi il descend | — | ✅ OK | Parente coherente : 4 depot(s), tous retrouves, tous porteurs d'un remote. | -| P41 | Resolution d'instance : une seule, partagee | — | ✅ OK | Resolution unique : 49 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). | +| P41 | Resolution d'instance : une seule, partagee | — | ✅ OK | Resolution unique : 50 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). | | P42 | L'edge porte les noms qu'il publie | — | ✅ OK | 4 edge(s) emettent un certificat portant les noms publies (OPS-Chezlepro-lab/principal, OPS-Chezlepro/principal, OPS-Technolibre/principal, OPS-Patient0/product | | P43 | Frontiere : le devis voit les machines du site | — | ✅ OK | Devis de la frontiere : 5 machine(s) du plan retrouvees, 66 regle(s) du site. | | P44 | Integrations : le serveur avant ses clients | — | ✅ OK | 4 integration(s) appliquent leur serveur avant leurs clients. | diff --git a/docs/procedure-template-debian13-proxmox.md b/docs/procedure-template-debian13-proxmox.md index 57e3f8e..5a79a26 100644 --- a/docs/procedure-template-debian13-proxmox.md +++ b/docs/procedure-template-debian13-proxmox.md @@ -58,6 +58,39 @@ BIOS : OVMF / UEFI Machine : q35 ``` +### `q35` n'est pas un réglage — c'est la raison de cette procédure + +**Ces deux lignes sont pourquoi l'installation est manuelle.** Elles expliquent aussi +pourquoi Set-OPS n'utilise pas l'image cloud officielle de Debian. + +`genericcloud` est livrée configurée pour **`i440fx`**, le défaut de Proxmox. La convertir +en `q35` après coup ne change pas un paramètre : ça **remplace le matériel virtuel sous un +système qui croit connaître le sien**. `i440fx` est un chipset PCI, `q35` est PCIe — la +topologie des bus change, donc : + +- les **noms d'interfaces prédictibles** changent, puisqu'ils dérivent du chemin PCI + (`enp0s3` devient `enp1s0`) — la machine perd le réseau, et sa configuration réseau + désigne une interface qui n'existe plus ; +- les **chemins de disques** bougent, ce qui peut valoir un initramfs qui ne trouve plus + sa racine ; +- l'ordre d'énumération des périphériques n'est plus le même, et ce qui en dépend suit. + +**Constat de l'exploitant, paye en anomalies** : une conversion `i440fx` → `q35` sur une +machine déjà installée produit une série de pannes dont chacune ressemble à autre chose +qu'à sa cause. *La conversion n'est pas une correction — c'est une transplantation.* + +D'où la règle : **une machine naît `q35`, ou elle ne le sera jamais proprement.** C'est ce +que cette installation depuis l'ISO garantit, et ce qu'une image préconfigurée pour +`i440fx` interdit. + +*Le gabarit hérite ces valeurs à chaque clonage — le vérifier avant de le convertir en +modèle est le dernier moment où la correction est gratuite :* + +```sh +qm config | grep -E '^machine|^bios' +# attendu : machine: q35 / bios: ovmf +``` + ### Disque EFI Proxmox Avec OVMF/UEFI, Proxmox crée un petit disque EFI, par exemple : diff --git a/scripts/gabarit_etat.py b/scripts/gabarit_etat.py new file mode 100755 index 0000000..53e4300 --- /dev/null +++ b/scripts/gabarit_etat.py @@ -0,0 +1,122 @@ +#!/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. `qm config` les rend en `cle: valeur`. +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'} »") + + 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}") + 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.") + return 1 + + print("\nConforme — le gabarit porte ce que le SITE declare de lui.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/underlay.py b/scripts/underlay.py index 059aec9..a790bd2 100644 --- a/scripts/underlay.py +++ b/scripts/underlay.py @@ -843,7 +843,7 @@ def main() -> None: if not g: return # `cle=valeur` par ligne : consommable par `eval` en shell comme par Ansible. - for k in ("vmid", "nom", "noeud"): + for k in ("vmid", "nom", "noeud", "machine", "bios"): if g.get(k) is not None: print(f"{k}={g[k]}") return