From 9aef3614abe48d6126c634049dc3ac2bd7bc2153 Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Tue, 1 Sep 2026 13:46:31 -0400 Subject: [PATCH] gabarit : refabrique, minimal, et puise aux ressources du SITE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit REFABRIQUE (VMID 9006, modeleSetOPS-minimal). Quatre roles au lieu de dix-sept : qemu_guest_agent, cloud_init, sudo_ansible, ssh_baseline — des conditions d existence, pas des choix d efficacite. Tout le reste vient du socle, et P56 refuse qu un role retire ne soit repris par personne. FABRIQUE CHEZ LE SITE, ET C EST LA DECISION QUI COMPTE. « Les ressources du SITE font autorite pour tous ses artefacts ; elles servent les tenants jusqu a ce qu ils s emancipent. » Il se fabriquait DEHORS : `-i ","` ne porte aucun group_vars, donc ni mandataire ni resolveur. L ancien gabarit allait chercher ses paquets chez Debian et resolvait chez l ancien LAN — 192.168.10.10, lu sur la VM 99998. Le site avait son cache et son resolveur, et son propre artefact les ignorait. Desormais la fabrication DERIVE ses ressources du plan du site (underlay --adresses) et tourne sur le reseau du genome. PROUVE : dix requetes de 10.0.33.31 servies par site-cache-01, resolveur pose a 10.0.34.11. L IDENTITE DU GABARIT VIENT DU SITE, PLUS DU TENANT. `proxmox_clone_vmid_modele` vivait dans les group_vars de l ecosysteme : deux tenants pouvaient cloner deux gabarits differents sans que rien ne le dise, et un tenant decidait d un objet dont descend chaque VM de chaque ecosysteme. Meme mouvement que l INDEX (2026-08-25) : le site ALLOUE, le tenant RECOIT. Cle `gabarit` du plan du site, lue par underlay --gabarit. PIEGE FERME EN CHEMIN : creer-vm passait VMID_MODELE="$SETOPS_VMID_MODELE" a cloner-vm, or inventory_host.py n emet PAS cette variable. Elle valait donc le VIDE, et ce vide ECRASAIT la valeur derivee — le gabarit du tenant reprenait la main sans bruit. ET UN PIEGE DEJA DOCUMENTE, PAYE UNE TROISIEME FOIS : le chemin du controleur contient une espace, et `lookup('pipe', ...)` le decoupait. Guillemets. EPROUVE DE BOUT EN BOUT : VM clonee du gabarit minimal — nom, adresse, machine-id neuf, cle d hote regeneree, agent invite actif ; puis socle applique dessus, changed=5, 0 echec, auditd compris. VM d essai retiree. make verifier : vert. make prouver : CONFORME, 56 OK, 0 echec. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on --- CHANGELOG.md | 48 +++++++++++++ Makefile | 21 +++++- docs/audit/preuve-2026-09-01.md | 2 +- .../modeles_vm/debian13_proxmox_preparer.yml | 69 +++++++++++++++++++ scripts/underlay.py | 37 ++++++++++ 5 files changed, 174 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c79ddf7..a2fdcb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,53 @@ # CHANGELOG — Set-OPS +## 2026-09-01 — Le gabarit refabrique : minimal, et fabrique chez le SITE + +**56 preuves.** Le gabarit dore est refait — VMID **9006**, `modeleSetOPS-minimal`, quatre +roles au lieu de dix-sept. + +### Il se fabriquait dehors + +*« Les ressources du SITE font autorite pour tous ses artefacts ; elles servent les tenants +jusqu'a ce qu'ils s'emancipent »* (decision de l'exploitant). Le gabarit en est un — c'est +de lui que descend **chaque VM de chaque tenant**. + +Or `-i ","` ne porte aucun `group_vars` : la fabrication n'avait ni mandataire ni +resolveur. L'ancien gabarit allait chercher ses paquets **chez Debian** et resolvait chez +**l'ancien LAN** (`192.168.10.10`, lu sur la VM 99998). Le site avait son cache et son +resolveur, et son propre artefact les ignorait. + +La fabrication **derive** desormais ses ressources du plan du site et tourne sur le reseau +du genome. *Prouve : dix requetes de `10.0.33.31` servies par `site-cache-01`.* + +### L'identite du gabarit vient du site, plus du tenant + +`proxmox_clone_vmid_modele` vivait dans les `group_vars` de l'ecosysteme. **Deux tenants +pouvaient donc cloner deux gabarits differents sans que rien ne le dise**, et un tenant +decidait d'un objet qui n'est pas a lui. + +*Meme mouvement que l'INDEX, tranche le 2026-08-25 : le site ALLOUE, le tenant RECOIT.* + +### Deux pieges fermes en chemin + +**Une valeur vide qui ecrasait la derivation.** `creer-vm` passait +`VMID_MODELE="$SETOPS_VMID_MODELE"` a `cloner-vm` — or `inventory_host.py` n'emet pas +cette variable. Elle valait le VIDE, et ce vide reprenait la main sur le site sans bruit. + +**Le chemin du controleur contient une espace**, et `lookup('pipe', …)` le decoupait. Ce +depot l'a paye une troisieme fois — `ansible-galaxy` le 08-23, `git bundle` le 08-26, ici +le 09-01. + +### Eprouve de bout en bout + +``` +VM clonee du gabarit minimal nom, adresse, machine-id neuf, + cle d'hote regeneree, agent invite actif +socle applique dessus changed=5, 0 echec, auditd compris +``` + +*VM d'essai retiree. L'ancien gabarit (99998) est garde et declare comme `precedent` : le +retirer est un geste, pas un effet, et il attend une reconstruction complete sur le neuf.* + ## 2026-09-01 — Le gabarit etait un cache du socle, et il perimait sans le dire **56 preuves.** Question de l'exploitant : le gabarit dore et cloud-init gagnent-ils leur diff --git a/Makefile b/Makefile index 85dd64d..74b2591 100644 --- a/Makefile +++ b/Makefile @@ -66,7 +66,24 @@ ADRESSE_IP ?= GROUPES ?= $(GROUPE_DEBIAN) GROUPE ?= $(GROUPE_DEBIAN) UTILISATEUR_ANSIBLE ?= ansible -VMID_MODELE ?= +# LE GABARIT VIENT DU SITE, PAS DU TENANT (2026-09-01). +# +# « Les ressources du SITE font autorite pour tous ses artefacts. » Le gabarit en est un : +# c'est de lui que descend chaque VM de chaque tenant. Son VMID vivait pourtant dans les +# group_vars de l'ecosysteme — deux tenants pouvaient donc cloner deux gabarits differents +# sans que rien ne le dise, et un tenant decidait d'un objet qui n'est pas a lui. +# +# Meme mouvement que l'INDEX, tranche le 2026-08-25 : le site ALLOUE, le tenant RECOIT. +# +# `?=` : une valeur donnee en ligne de commande gagne — necessaire pour eprouver un +# gabarit neuf avant de le declarer. Et si le site n'en declare aucun, la valeur reste +# vide et le plan du tenant reprend la main : degrader, jamais deviner. +VMID_MODELE ?= $(shell python3 scripts/underlay.py --gabarit 2>/dev/null | sed -n 's/^vmid=//p') +# NOTE SUR LE PIEGE QU'ON VIENT DE FERMER : `creer-vm` passait +# `VMID_MODELE="$$SETOPS_VMID_MODELE"` a `cloner-vm`, or `inventory_host.py` n'emet PAS +# cette variable — elle valait donc le VIDE, et ce vide ECRASAIT la valeur derivee. Le +# gabarit du tenant reprenait la main sans que rien ne le dise. `:-` rend la derivation au +# site quand l'inventaire ne dit rien. VMID ?= NOEUD_PROXMOX ?= STOCKAGE_PROXMOX ?= @@ -970,7 +987,7 @@ site-creer: ## Cree les machines du site depuis l'underlay — CONFIRMER=true COEURS="$$SETOPS_COEURS" \ MEMOIRE="$$SETOPS_MEMOIRE" \ NOEUD_PROXMOX="$$SETOPS_NOEUD" \ - VMID_MODELE="$$SETOPS_VMID_MODELE" \ + VMID_MODELE="$${SETOPS_VMID_MODELE:-$(VMID_MODELE)}" \ CLONE_COMPLET="$$SETOPS_CLONE_COMPLET" \ FORMAT_DISQUE="$(FORMAT_DISQUE)"; \ printf '=== %s : %s s ===\n' "$$m" "$$(( $$(date +%s) - t0 ))"; \ diff --git a/docs/audit/preuve-2026-09-01.md b/docs/audit/preuve-2026-09-01.md index 992eac1..8540aa7 100644 --- a/docs/audit/preuve-2026-09-01.md +++ b/docs/audit/preuve-2026-09-01.md @@ -44,7 +44,7 @@ | 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. | -| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 41 exigence(s) de role, toutes satisfaites (130 cle(s) declaree(s) par l'instance). | +| 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)). | | P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 5 application(s) exigeant une base l'ont toutes (4 entree(s) au registre). | diff --git a/playbooks/modeles_vm/debian13_proxmox_preparer.yml b/playbooks/modeles_vm/debian13_proxmox_preparer.yml index dd81cdf..b915187 100644 --- a/playbooks/modeles_vm/debian13_proxmox_preparer.yml +++ b/playbooks/modeles_vm/debian13_proxmox_preparer.yml @@ -10,6 +10,75 @@ gather_facts: true pre_tasks: + # ─── LE GABARIT EST UN ARTEFACT DU SITE, ET IL SE FABRIQUE CHEZ LUI ────────── + # + # « Les ressources du SITE font autorité pour tous les artefacts du site » (décision + # de l'exploitant, 2026-09-01). Le gabarit en est un — c'est de lui que descend + # chaque VM de chaque tenant. + # + # Il se fabriquait pourtant DEHORS : `-i ","` ne porte aucun `group_vars`, donc + # ni mandataire ni résolveur. La machine allait chercher ses paquets chez Debian et + # résolvait chez l'ancien LAN (`192.168.10.10`, lu sur le gabarit du 2026-08-09). + # Le site avait son cache et son résolveur, et son propre artefact les ignorait. + # + # DÉRIVÉ, JAMAIS ÉCRIT : les adresses viennent du plan du site, par le groupe que la + # machine porte. Le jour où le cache déménage, la fabrication suit — c'est déjà + # arrivé une fois, au découpage du site en zones. + - name: Où le SITE sert-il ses paquets et ses noms ? + ansible.builtin.set_fact: + # LE CHEMIN DU CONTROLEUR PEUT CONTENIR UNE ESPACE — « Espace Chezlepro/… » + # chez le mainteneur — et `pipe` passe sa chaine au shell. Sans guillemets, il + # decoupe le chemin et rend 2. Meme piege que `ansible-galaxy` le 2026-08-23 et + # que `git bundle` le 2026-08-26 : ce depot l'a paye trois fois. + modele_cache_site: >- + {{ (lookup('pipe', 'python3 "' ~ playbook_dir ~ '/../../scripts/underlay.py" --adresses serveur_artefacts') + | trim | split('\n') | first | default('', true)) }} + modele_resolveur_site: >- + {{ (lookup('pipe', 'python3 "' ~ playbook_dir ~ '/../../scripts/underlay.py" --adresses serveur_resolveur') + | trim | split('\n') | first | default('', true)) }} + delegate_to: localhost + become: false + # PAS de `run_once` : la fabrication ne vise qu'une machine de toute facon, et + # `run_once` se comporte autrement sous la strategie `free` — une garde qui depend + # de la strategie n'en est pas une. + + - name: Exiger que le site offre ce qu'il faut pour fabriquer son gabarit + ansible.builtin.assert: + that: + - modele_cache_site | length > 0 + - modele_resolveur_site | length > 0 + fail_msg: >- + Le plan du SITE ne déclare pas de `serveur_artefacts` et/ou de + `serveur_resolveur`. Le gabarit est un artefact du site : le fabriquer sans eux + le ferait dépendre d'Internet et de l'ancien LAN, alors que tout ce dont il a + besoin doit venir du site. + + # UNE ADRESSE, PAS UN NOM : au moment où ces deux fichiers servent, la machine ne + # résout encore rien — c'est justement ce qu'on est en train de lui donner. + - name: Poser le résolveur du SITE + ansible.builtin.copy: + dest: /etc/resolv.conf + owner: root + group: root + mode: "0644" + content: | + # Géré par Set-OPS pendant la FABRICATION du gabarit — le résolveur du SITE. + # `template_cleanup` le remet à zéro : un gabarit ne transporte pas d'adresse. + nameserver {{ modele_resolveur_site }} + + - name: Poser le cache d'artefacts du SITE + ansible.builtin.copy: + dest: /etc/apt/apt.conf.d/00-setops-amorcage-artefacts + owner: root + group: root + mode: "0644" + content: | + // Géré par Set-OPS pendant la FABRICATION du gabarit — le cache du SITE. + // Les VM clonées le remplacent par celui de leur écosystème (client_artefacts). + Acquire::http::Proxy "http://{{ modele_cache_site }}:3142"; + // `DIRECT` obligatoire : apt fait HÉRITER la valeur HTTPS de la valeur HTTP. + Acquire::https::Proxy "DIRECT"; + - name: Vérifier que la cible est Debian ansible.builtin.assert: that: diff --git a/scripts/underlay.py b/scripts/underlay.py index ddc3adf..059aec9 100644 --- a/scripts/underlay.py +++ b/scripts/underlay.py @@ -141,6 +141,22 @@ def services_site() -> dict[str, list[str]]: return out +def gabarit() -> dict: + """Le gabarit dore, tel que le SITE le declare — VMID, nom, noeud. + + LE GABARIT EST UN ARTEFACT DU SITE (2026-09-01). C'est de lui que descend chaque VM de + chaque tenant ; son identite ne peut donc pas etre decidee par un tenant. Elle l'etait : + `proxmox_clone_vmid_modele` vivait dans les group_vars de l'ecosysteme, si bien que + deux tenants pouvaient cloner deux gabarits differents sans que rien ne le dise. + + Meme mouvement que l'INDEX, tranche le 2026-08-25 : le site ALLOUE, le tenant RECOIT. + + Rend {} quand la carte de l'hebergeur n'est pas montee : l'ecosysteme retombe alors sur + ce que son propre plan declare, comme avant. Degrader, jamais deviner. + """ + return dict((lire_plan_site("10-intrants.yml") or {}).get("gabarit") or {}) + + def adresses_site_portant(groupe: str) -> list[str]: """Les adresses des machines du SITE qui portent ce groupe. @@ -813,7 +829,28 @@ def afficher() -> None: def main() -> None: ap = argparse.ArgumentParser(description=__doc__) ap.add_argument("--verifier", action="store_true", help="valide l'underlay (P23), code de sortie") + # LE SITE, INTERROGEABLE DEPUIS UN PLAYBOOK. La fabrication du gabarit doit puiser aux + # ressources du site (« elles font autorite pour tous ses artefacts »), et elle tourne + # sur un inventaire d'un seul hote — sans `group_vars`, donc sans rien deriver + # elle-meme. Elle demande ici, plutot que d'ecrire une adresse qui vieillirait. + ap.add_argument("--gabarit", action="store_true", + help="le gabarit dore declare par le SITE (vmid, nom, noeud)") + ap.add_argument("--adresses", metavar="GROUPE", + help="adresses des machines du SITE portant ce groupe, une par ligne") args = ap.parse_args() + if args.gabarit: + g = gabarit() + if not g: + return + # `cle=valeur` par ligne : consommable par `eval` en shell comme par Ansible. + for k in ("vmid", "nom", "noeud"): + if g.get(k) is not None: + print(f"{k}={g[k]}") + return + if args.adresses: + for a in adresses_site_portant(args.adresses): + print(a) + return if args.verifier: sys.exit(verifier()) afficher()