collections : epingler les versions — le runner echouait la ou le poste reussissait

Premiere materialisation de VM depuis le runner du SITE :

    ERROR! couldn't resolve module/action 'community.general.proxmox_pool'

Meme depot, meme playbook, meme plan que chez le mainteneur. La difference tenait
a une seule chose que le moteur ne disait pas : la VERSION de ses collections.

Le poste porte `community.general` 10.3.0. Le runner, monte un jour plus tard, a
recu la 13.3.0 — et la version 11 a RETIRE les modules Proxmox de cette collection
(ils vivent desormais dans `community.proxmox`). `requirements.yml` nommait ses
collections sans dire lesquelles : chaque machine installait donc ce qui etait
courant le jour de son montage. Une dependance non epinglee n'est pas une
dependance, c'est un pari sur l'etat d'Internet a la date du deploiement.

TROIS CORRECTIONS.

`requirements.yml` epingle les trois collections aux versions eprouvees.

`serveur_ops` installe avec `--force`. Sans lui, ansible-galaxy laisse en place une
version SUPERIEURE a celle demandee : il ne retrograde pas. Un poste peut etre en
avance, pas seulement en retard, et le depot doit faire autorite dans les deux sens.

P51 garde les deux faiblesses de ce fichier — celle du 2026-08-24, une collection
utilisee sans etre declaree (`ansible.posix`), et celle d'aujourd'hui, declaree sans
version. La preuve ne lit que les fichiers de TACHES : un `defaults/main.yml` porte
`net.ipv4.ip_forward`, qu'un motif trop large prend pour un module.

MIGRATION CONNUE, PAS FAITE : passer les quatre modules Proxmox a
`community.proxmox` permettra de suivre `community.general` au-dela de la 11.

make prouver : CONFORME, 51 OK, 0 echec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-08-27 18:58:23 -04:00
parent 3b65384b0f
commit 3fa6e4c3e1
4 changed files with 114 additions and 2 deletions

View file

@ -7,7 +7,7 @@
> [`docs/audit/affirmations.md`](affirmations.md).
- **Instance** : `instance` — inventaire `instance/inventories/production/hosts.yml`
- **Verdict** : ✅ CONFORME (50 OK · 0 echec · 0 saute)
- **Verdict** : ✅ CONFORME (51 OK · 0 echec · 0 saute)
## Preuves
@ -63,6 +63,7 @@
| P48 | La carte d'orientation designe ce qui existe, et compte juste | — | ✅ OK | La carte designe 84 chemin(s) qui existent, et ses 7 chiffres correspondent a la mesure. |
| P49 | Registre des flux : la matrice d'audit est a jour | — | ✅ OK | Le registre des flux reproduit exactement ce que les `meta/flux.yml` declarent (110 lignes). |
| P50 | Silences : un refus muet est declare, place en dernier, et motive | — | ✅ OK | 2 silence(s) declare(s), tous en sequence > 1 (la plus haute des 121 regles `pass`), tous non consignes et tous motives. |
| P51 | Collections : toutes declarees, toutes epinglees | — | ✅ OK | 3 collection(s) appelee(s) par le moteur, toutes declarees et epinglees : ansible.posix==1.6.2, community.general==10.3.0, community.postgresql==3.10.2 |
## Couverture des affirmations ✅ du registre

View file

@ -7,12 +7,36 @@
# poste du mainteneur. Une collection utilisee par un role et absente d'ici marche chez
# le mainteneur et echoue partout ailleurs : c'est le genre d'ecart qu'on ne voit qu'en
# portant le moteur sur une autre machine.
# LES VERSIONS SONT EPINGLEES (2026-08-27). Ce fichier nommait ses collections sans dire
# LESQUELLES : chaque machine installait donc ce qui etait courant le jour de son montage.
#
# Ce que ca a coute. Le runner du SITE, monte le 2026-08-26, a recu `community.general`
# 13.3.0 quand le poste du mainteneur porte la 10.3.0. Or la version 11 a RETIRE les
# modules Proxmox de cette collection — ils vivent desormais dans `community.proxmox`. Le
# runner a donc echoue a la premiere materialisation de VM, sur un
# « couldn't resolve module/action 'community.general.proxmox_pool' », alors que le meme
# depot, le meme playbook et le meme plan fonctionnaient chez le mainteneur.
#
# Un moteur qui ne dit pas de QUELLES versions il depend n'est pas portable : il est
# seulement chanceux. Nommer une collection sans l'epingler, c'est declarer une dependance
# sur « l'etat d'Internet a la date du deploiement ».
#
# MIGRATION CONNUE, PAS ENCORE FAITE : passer les quatre modules Proxmox
# (`proxmox_kvm`, `proxmox_pool`, `proxmox_nic`, `proxmox_disk`) a `community.proxmox`
# permettra de suivre `community.general` au-dela de la 11. Tant que ce n'est pas fait,
# l'epingle ci-dessous est ce qui tient. Les modules LDAP, eux, restent dans
# `community.general` — la migration ne les concerne pas.
collections:
- name: community.postgresql
version: 3.10.2
# `<11.0.0` N'EST PAS UN CAPRICE : la 11 a sorti les modules Proxmox de cette collection.
# Les remonter demanderait de reecrire `playbooks/proxmox/*` — voir la note ci-dessus.
- name: community.general
version: 10.3.0
# `serveur_backup` : ansible.posix.authorized_key. Utilisee depuis longtemps, jamais
# declaree — donc absente du runner, qui n'aurait pas pu deployer les sauvegardes.
- name: ansible.posix
version: 1.6.2
# `community.docker` a ete RETIREE le 2026-08-24 : `serveur_collabora` etait la seule
# exception conteneurisee de la flotte, et il tourne desormais en natif (paquet coolwsd
# du depot amont Collabora). Plus aucun role n'a besoin de Docker.

View file

@ -237,9 +237,18 @@
ansible.builtin.command:
cmd: >-
{{ serveur_ops_venv }}/bin/ansible-galaxy collection install
-r requirements.yml
-r requirements.yml --force
-p {{ serveur_ops_racine }}/.ansible/collections
chdir: "{{ serveur_ops_racine }}/.collections-hors-ligne"
# `--force` PARCE QU'UN POSTE PEUT ETRE EN AVANCE, PAS SEULEMENT EN RETARD (2026-08-27).
# Sans lui, `ansible-galaxy` laisse en place une collection deja installee dans une
# version SUPERIEURE a celle demandee : il ne retrograde pas. Le runner du SITE, monte
# avant que `requirements.yml` n'epingle ses versions, portait `community.general` 13.3.0
# — une version d'ou les modules Proxmox ont ete RETIRES. La premiere materialisation de
# VM depuis le runner a echoue la-dessus. Le depot doit faire autorite dans les DEUX
# sens : ce que `requirements.yml` declare est ce qui est installe, ni plus haut ni plus
# bas. Voir la preuve P51.
#
# LE GARDE-FOU SUIVAIT LA MAUVAISE CHOSE (2026-08-24). Il etait `creates: …/community/general` :
# une collection AJOUTEE a requirements.yml n'aurait jamais ete installee, puisque le
# repertoire temoin existait deja. Le runner serait reste en retard sur le moteur, en

View file

@ -1396,6 +1396,82 @@ def preuve_carte_dit_vrai() -> tuple[bool, str]:
f"{len(_carte_chiffres_mesures())} chiffres correspondent a la mesure.")
def preuve_collections_declarees_et_epinglees() -> tuple[bool, str]:
"""Toute collection appelee par le moteur est DECLAREE, et a une version EPINGLEE.
DEUX PANNES, LA MEME CAUSE — le moteur ne disait pas de quoi il depend.
Le 2026-08-24, `ansible.posix` etait utilisee depuis longtemps sans figurer dans
`requirements.yml` : elle trainait sur le poste du mainteneur, et manquait partout
ailleurs. Le runner n'aurait pas pu deployer les sauvegardes.
Le 2026-08-27, la meme faiblesse a frappe d'un autre cote. Les collections etaient
nommees mais SANS VERSION : le runner du site, monte un jour plus tard, a recu
`community.general` 13.3.0 quand le poste porte la 10.3.0. La version 11 ayant sorti
les modules Proxmox de cette collection, la premiere materialisation de VM depuis le
runner a echoue sur `couldn't resolve module/action community.general.proxmox_pool`
— avec le meme depot, le meme playbook et le meme plan que chez le mainteneur.
Une dependance non epinglee n'est pas une dependance : c'est un pari sur l'etat
d'Internet a la date du deploiement. Un moteur qui se veut portable, et exploitable
par un sysadmin sans IA, doit pouvoir se reconstruire a l'identique dans six mois.
On lit les MODULES REELLEMENT APPELES dans les fichiers YAML — la clef d'une tache,
pas une chaine dans du Python : `ansible.parsing.vault` est un import, pas une
collection, et l'y confondre ferait crier la preuve pour rien.
"""
req = RACINE / "requirements.yml"
if not req.is_file():
return False, "requirements.yml est introuvable — le moteur ne declare aucune dependance."
try:
import yaml as _y
decl = (_y.safe_load(req.read_text(encoding="utf-8")) or {}).get("collections") or []
except Exception as e: # noqa: BLE001
return False, f"requirements.yml est illisible : {e}"
declarees = {}
for c in decl:
if isinstance(c, dict) and c.get("name"):
declarees[str(c["name"])] = str(c.get("version") or "").strip()
elif isinstance(c, str):
declarees[c] = ""
# Une clef de tache YAML : ` - community.general.ldap_entry:` ou ` module.x.y:`
motif = re.compile(r"^\s*(?:-\s+)?([a-z0-9_]+\.[a-z0-9_]+)\.[a-z0-9_]+\s*:", re.M)
# ON NE LIT QUE LES FICHIERS DE TACHES. Un `defaults/main.yml` peut contenir
# `net.ipv4.ip_forward: 1` — une clef de sysctl que le motif prendrait pour un module,
# et la preuve crierait a une collection manquante nommee `net.ipv4`. Un module ne
# peut apparaitre que la ou il y a des taches.
fichiers = list((RACINE / "playbooks").rglob("*.yml"))
for sous in ("tasks", "handlers"):
fichiers += list((RACINE / "roles").rglob(f"{sous}/*.yml"))
utilisees: dict[str, str] = {}
for f in fichiers:
try:
txt = f.read_text(encoding="utf-8")
except Exception: # noqa: BLE001
continue
for coll in motif.findall(txt):
if coll == "ansible.builtin":
continue
utilisees.setdefault(coll, str(f.relative_to(RACINE)))
manquantes = [f"{c} (ex. {utilisees[c]})" for c in sorted(utilisees) if c not in declarees]
sans_epingle = [c for c in sorted(declarees) if not declarees[c]]
fautes = []
if manquantes:
fautes.append("appelee(s) mais NON DECLAREE(S) dans requirements.yml : "
+ ", ".join(manquantes))
if sans_epingle:
fautes.append("declaree(s) SANS VERSION — le deploiement prendra ce qui passe : "
+ ", ".join(sans_epingle))
if fautes:
return False, " | ".join(fautes)
return True, (f"{len(utilisees)} collection(s) appelee(s) par le moteur, toutes "
f"declarees et epinglees : "
+ ", ".join(f"{c}=={declarees[c]}" for c in sorted(utilisees)))
def preuve_silences_declares() -> tuple[bool, str]:
"""Un `block` muet doit etre DECLARE, place APRES les `pass`, et porter son motif.
@ -1854,6 +1930,8 @@ PREUVES: list[dict] = [
"refs": [], "func": preuve_registre_flux_a_jour},
{"id": "P50", "titre": "Silences : un refus muet est declare, place en dernier, et motive",
"refs": [], "func": preuve_silences_declares},
{"id": "P51", "titre": "Collections : toutes declarees, toutes epinglees",
"refs": [], "func": preuve_collections_declarees_et_epinglees},
{"id": "P43", "titre": "Frontiere : le devis voit les machines du site", "refs": [],
"func": preuve_devis_frontiere_du_site},
{"id": "P33", "titre": "Aucune collision de port entre roles co-localises", "refs": [],