gabarit : refabrique, minimal, et puise aux ressources du SITE
Some checks are pending
verifier / verifier (push) Waiting to run

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 "<ip>,"` 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
This commit is contained in:
Daniel Allaire 2026-09-01 13:46:31 -04:00
parent 2f2323bd3e
commit 9aef3614ab
5 changed files with 174 additions and 3 deletions

View file

@ -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 "<ip>,"` 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

View file

@ -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 ))"; \

View file

@ -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). |

View file

@ -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 "<ip>,"` 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:

View file

@ -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()